// Copyright (c) 2021 Giorgos Vougioukas
//
// The license can be found in the LICENSE file.

#include "thalia/main_wnd.h"

#define THA_MAIN_SCREEN_UPDATE 300
#define THA_MAIN_SCREEN_UPDATE_MS 50
#define THA_CLEAR_EDIT 310
#define THA_CLEAR_EDIT_MS 10
#define THA_CLEAR_INFO 320
#define THA_CLEAR_INFO_MS 1000
#define THA_ORIGINAL_TITLE 330
#define THA_ORIGINAL_TITLE_MS 1200
#define THA_DISABLE_CONTROLS 340
#define THA_DISABLE_CONTROLS_MS 50

#include "thalia/render_system.h"
#include "thalia/app_info.h"
#include "thalia/plugin.h"
#include "thalia/about_wnd.h"
#include "thalia/changelog_wnd.h"
#include "thalia/license_wnd.h"
#include "thalia/preferences.h"
#include "thalia/preferences_wnd.h"
#include "thalia/playlist_wnd.h"
#include "thalia/metadata_wnd.h"
#include "thalia/artwork_wnd.h"
#include "thalia/metadata_queue.h"
#include "thalia/database.h"
#include "thalia/playlist.h"
#include "thalia/query.h"
#include "thalia/track.h"

#include "WDL/fpcmp.h"
#include "WDL/wdlstring.h"
#include "WDL/time_precise.h"
#include "WDL/filebrowse.h"
#include "WDL/lice/lice.h"
#include "WDL/wingui/virtwnd.h"
#include "WDL/wingui/scrollbar/coolscroll.h"
#include "WDL/wdlutf8.h"
#include "WDL/time_precise.h"
#include "WDL/dirscan.h"

#define THA_OPEN_MEDIA_FILES \
  "All supported media files\0*.flac;*.mp3;*.ogg;*.wv;*.wav;*.aiff;*.opus\0" \
  "FLAC audio files (*.flac)\0*.flac\0" \
  "MPEG audio files (*.mp3)\0*.mp3\0" \
  "Vorbis audio files (*.ogg)\0*.ogg\0" \
  "WavPack audio files (*.wv)\0*.wv\0" \
  "WAV audio files (*.wav)\0*.wav\0" \
  "AIFF audio files (*.aiff)\0*.aiff\0" \
  "Opus audio files (*.opus)\0*.opus\0" \
  "All files (*.*)\0*.*\0\0"
#define THA_OPEN_MEDIA_FILE_DEFEXT "mp3"

static WNDPROC PrevTreeProc = NULL;
static HWND s_tree = NULL;
static WNDPROC PrevListProc = NULL;
static HWND s_list = NULL;
static WNDPROC PrevEditProc = NULL;
static HWND s_edit = NULL;
static bool s_istreeroot = false;

static LOGFONT lf =
{
#if defined(_WIN32)
  14, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
  OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH,
  "Arial"
#else
  11, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
  OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH,
  "Arial"
#endif
};

THA_MainWnd::THA_MainWnd()
  : m_hwnd(NULL)
  , m_x(0)
  , m_y(0)
  , m_w(0)
  , m_h(0)
  , m_main_menu(NULL)
  , m_tree(NULL)
  , m_item(NULL)
  , m_list(NULL)
  , m_edit(NULL)
  , m_editlabel(NULL)
  , m_column_menu(NULL)
  , m_database_menu(NULL)
  , m_playlist_menu(NULL)
  , m_time_menu(NULL)
  , m_lv_drag(false)
  , m_dragitem(-1)
  , m_selstart(-1)
  , m_selend(-1)
  , m_activesel(-1)
  , m_splitter(false)
  , m_scandb(false)
  , m_time(NULL)
  , m_info(NULL)
  , m_font(NULL)
  , m_timestate(false)
  , m_brush(NULL)
  , m_playlistfind(false)
  , m_marquee(-1)
  , m_titlecapture(false)
{}

THA_MainWnd::~THA_MainWnd()
{
  DestroyWindow(m_hwnd);
  delete m_font;
  if (m_brush) DeleteObject(m_brush);
}

void THA_MainWnd::Search()
{
  m_editlabel->SetText("DB:");

  char text[2048];
  WDL_String str;
  WDL_FastString sql;
  GetWindowText(m_edit, text, sizeof(text));

  str.Set(text);
  WDL_TypedBuf<int> qp;

  for (int j = 0; j < str.GetLength(); j++)
  {
    char *quote = str.Get();

    if (quote[j] == '\'')
    {
      // 1 2 3 4 5 6 7 8 9 10
      //         '          '
      int pos = j + qp.GetSize();
      qp.Add(pos);
    }
  }

  for (int k = 0; k < qp.GetSize(); k++)
  {
    str.Insert("'", qp.Get()[k]);
  }

  bool whitespace = true;
  for (int i = 0; i < str.GetLength(); i++)
  {
    if (str.Get()[i] == ' ') continue; else { whitespace = false; break; }
  }

  if (!whitespace)
  {
    double wall0 = time_precise();
    g_query->Query(text);
    double wall1 = time_precise();
    m_strbuf.SetFormatted(512, "%s | DB query in %f sec",
      m_titleorig.Get(), wall1 - wall0);
    SetTitle(m_strbuf.Get());
  }
  else
  {
    g_query->Query();
  }
}

void THA_MainWnd::MoveUp()
{
  if (m_selstart >= 0) return;
  WDL_TypedBuf<int> selected;

  int dragitem = 0;
  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  if (selected.GetSize() && selected.Get()[0] > 0)
  {
    dragitem = selected.Get()[0] - 1;
    m_selstart = dragitem;
    if (m_selstart > selected.Get()[0])
    {
      m_selstart -= selected.GetSize() - 1;
    }
    m_selend = m_selstart + selected.GetSize();
    g_playlist->MoveSelection(&selected, dragitem);
  }
}

void THA_MainWnd::MoveDown()
{
  if (m_selstart >= 0) return;
  WDL_TypedBuf<int> selected;

  int dragitem = 0;
  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  if (selected.GetSize() && selected.Get()[selected.GetSize() - 1] <
    ListView_GetItemCount(m_list) - 1)
  {
    dragitem = selected.Get()[selected.GetSize() - 1] + 1;
    m_selstart = dragitem;
    if (m_selstart > selected.Get()[0])
    {
      m_selstart -= selected.GetSize() - 1;
    }
    m_selend = m_selstart + selected.GetSize();
    g_playlist->MoveSelection(&selected, dragitem);
  }
}

void THA_MainWnd::RemoveSelected()
{
  double wall0 = time_precise();
  WDL_TypedBuf<int> selected;

  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  if (selected.GetSize())
  {
    g_playlist->RemoveSelection(&selected);

    SendMessage(m_list, WM_SETREDRAW, FALSE, 0);

    for (int i = 0; i < selected.GetSize(); i++)
    {
      int sel = selected.Get()[i] - i;
      ListView_DeleteItem(m_list, sel);
    }

    int selmax = ListView_GetItemCount(m_list);
    m_selstart = wdl_clamp(selected.Get()[0], 0, selmax);
    m_selend = wdl_clamp(m_selstart + 1, 0, selmax);
    if (m_selend >= selmax)
    {
      m_selstart = wdl_clamp(selmax - 1, 0, selmax);
      m_selend = wdl_clamp(selmax, 0, selmax);
    }

    ApplySelection(); UpdateIndexes();
    SendMessage(m_list, WM_SETREDRAW, TRUE, 0);

    double wall1 = time_precise();
    if (selected.GetSize() == 1)
    {
      m_strbuf.SetFormatted(512, "%s | %d file removed in %f sec",
        m_titleorig.Get(), selected.GetSize(), wall1 - wall0);
      SetTitle(m_strbuf.Get());
    }
    else
    {
      m_strbuf.SetFormatted(512, "%s | %d files removed in %f sec",
        m_titleorig.Get(), selected.GetSize(), wall1 - wall0);
      SetTitle(m_strbuf.Get());
    }
  }
}

void THA_MainWnd::CropSelected()
{
  double wall0 = time_precise();
  WDL_TypedBuf<int> selected;

  for (int i = 0; i < ListView_GetItemCount(m_list); i++)
  {
    if (ListView_GetItemState(m_list, i, LVIS_SELECTED) == LVIS_SELECTED)
    {
      continue;
    }
    else
    {
      selected.Add(i);
    }
  }

  if (selected.GetSize())
  {
    g_playlist->RemoveSelection(&selected);

    SendMessage(m_list, WM_SETREDRAW, FALSE, 0);

    for (int i = 0; i < selected.GetSize(); i++)
    {
      int sel = selected.Get()[i] - i;
      ListView_DeleteItem(m_list, sel);
    }

    int selmax = ListView_GetItemCount(m_list);
    m_selstart = 0;
    m_selend = m_selstart + selmax;

    ApplySelection(); UpdateIndexes();
    SendMessage(m_list, WM_SETREDRAW, TRUE, 0);

    double wall1 = time_precise();
    if (selected.GetSize() == 1)
    {
      m_strbuf.SetFormatted(512, "%s | %d file removed in %f sec",
        m_titleorig.Get(), selected.GetSize(), wall1 - wall0);
      SetTitle(m_strbuf.Get());
    }
    else
    {
      m_strbuf.SetFormatted(512, "%s | %d files removed in %f sec",
        m_titleorig.Get(), selected.GetSize(), wall1 - wall0);
      SetTitle(m_strbuf.Get());
    }
  }
}

void THA_MainWnd::Sort(int subitem, bool reverse, bool selected)
{
  double wall0 = time_precise();
  if (selected)
  {
    WDL_TypedBuf<int> selection;
    WDL_ASSERT(m_sel.GetSize() == 0);

    int nit = 0;
    int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
    while (pos != -1)
    {
      nit++;
      if (nit > 1) break;
      pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
    }

    if (nit <= 1) return;

    pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
    while (pos != -1)
    {
      selection.Add(pos);
      m_sel.Add(pos);
      pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
    }

    if (selection.GetSize())
    {
      g_playlist->Sort(subitem, reverse, selected, &selection);
    }
  }
  else
  {
    m_selstart = 0;
    m_selend = m_selstart + 1;
    g_playlist->Sort(subitem, reverse, selected, NULL);
  }
  double wall1 = time_precise();
  m_strbuf.SetFormatted(512, "%s | Sort in %f sec",
    m_titleorig.Get(), wall1 - wall0);
  SetTitle(m_strbuf.Get());
}

void THA_MainWnd::ReverseSelected()
{
  double wall0 = time_precise();
  WDL_TypedBuf<int> selected;
  WDL_ASSERT(m_sel.GetSize() == 0);

  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    m_sel.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  if (selected.GetSize())
  {
    g_playlist->ReverseSelection(&selected);
  }
  double wall1 = time_precise();
  m_strbuf.SetFormatted(512, "%s | Reverse in %f sec",
    m_titleorig.Get(), wall1 - wall0);
  SetTitle(m_strbuf.Get());
}

void THA_MainWnd::RandomizeSelected()
{
  double wall0 = time_precise();
  WDL_TypedBuf<int> selected;
  WDL_ASSERT(m_sel.GetSize() == 0);

  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    m_sel.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  if (selected.GetSize())
  {
    g_playlist->RandomizeSelection(&selected);
  }
  double wall1 = time_precise();
  m_strbuf.SetFormatted(512, "%s | Randomize in %f sec",
    m_titleorig.Get(), wall1 - wall0);
  SetTitle(m_strbuf.Get());
}

void THA_MainWnd::SelectAll()
{
  for (int i = 0; i < ListView_GetItemCount(m_list); i++)
  {
    ListView_SetItemState(m_list, i, LVNI_SELECTED, LVNI_SELECTED);
  }
}

void THA_MainWnd::SelectNone()
{
  for (int i = 0; i < ListView_GetItemCount(m_list); i++)
  {
    ListView_SetItemState(m_list, i, ~LVNI_SELECTED, LVNI_SELECTED);
    ListView_SetItemState(m_list, i, ~LVNI_FOCUSED, LVNI_FOCUSED);
  }
}

void THA_MainWnd::InvertSelection()
{
  for (int i = 0; i < ListView_GetItemCount(m_list); i++)
  {
    if (ListView_GetItemState(m_list, i, LVIS_SELECTED))
    {
      ListView_SetItemState(m_list, i, ~LVNI_SELECTED, LVNI_SELECTED);
    }
    else
    {
      ListView_SetItemState(m_list, i, LVNI_SELECTED, LVNI_SELECTED);
    }
  }

  int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
  if (pos >= 0)
  {
    ListView_SetItemState(m_list, pos, LVNI_FOCUSED, LVNI_FOCUSED);
  }
}

void THA_MainWnd::DeleteAll()
{
  ListView_DeleteAllItems(m_list);
  g_playlist->DeleteAll();
}

void THA_MainWnd::OpenFileLocation()
{
  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED | LVNI_FOCUSED);
  if (pos >= 0)
  {
    int subitem = 0;
    char text[2048];
    ColumnHeader *ch = m_column_header.Get();

    for (int i = 0; i < m_column_header.GetSize(); i++)
    {
      if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
      {
        subitem = i;
        break;
      }
    }

    ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

    WDL_FastString fn(text);
    fn.remove_filepart();

    ShellExecute(m_hwnd, "", "explorer.exe", fn.Get(), "", SW_SHOWNORMAL);
  }
}

void THA_MainWnd::Cut()
{
  WDL_TypedBuf<int> selected;

  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  if (selected.GetSize())
  {
    m_selstart = (selected.Get()[0] - 1) < 0 ?
      0 : selected.Get()[0] - 1;
    m_selend = m_selstart + 1;
    g_playlist->CutSelection(&selected);

    SendMessage(m_list, WM_SETREDRAW, FALSE, 0);

    for (int i = 0; i < selected.GetSize(); i++)
    {
      int sel = selected.Get()[i] - i;
      ListView_DeleteItem(m_list, sel);
    }

    int selmax = ListView_GetItemCount(m_list);
    m_selstart = wdl_clamp(selected.Get()[0], 0, selmax);
    m_selend = wdl_clamp(m_selstart + 1, 0, selmax);
    if (m_selend >= selmax)
    {
      m_selstart = wdl_clamp(selmax - 1, 0, selmax);
      m_selend = wdl_clamp(selmax, 0, selmax);
    }

    ApplySelection(); UpdateIndexes();
    SendMessage(m_list, WM_SETREDRAW, TRUE, 0);

    m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes cut",
      m_titleorig.Get(), g_playlist->GetClipboardBytes());
    SetTitle(m_strbuf.Get());
  }
}

void THA_MainWnd::Copy()
{
  WDL_TypedBuf<int> selected;

  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  if (selected.GetSize())
  {
    g_playlist->CopySelection(&selected);

    m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
      m_titleorig.Get(), g_playlist->GetClipboardBytes());
    SetTitle(m_strbuf.Get());
  }
}

void THA_MainWnd::Paste()
{
  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED|LVNI_FOCUSED);
  if (ListView_GetItemCount(m_list) == 0) pos = 0;
  if (pos >= 0 && g_playlist->GetPasteSize())
  {
    g_playlist->Paste(pos);

    WDL_PtrList<THA_PlayList::PlayListEntry> *entries = g_playlist->GetClipboard();

    SendMessage(m_list, WM_SETREDRAW, FALSE, 0);

    for (int i = 0; i < entries->GetSize(); i++)
    {
      THA_PlayList::PlayListEntry *ple = entries->Get(i);

      LVITEM lvi = { 0, };
      int si = 0;
      lvi.mask = LVIF_TEXT | LVIF_PARAM;
      lvi.lParam = (LPARAM)i;
      lvi.iItem = pos + i;
      lvi.iSubItem = si++; // type
      lvi.pszText = (char *)ple->type.Get();
      lvi.cchTextMax = ple->type.GetLength();
      ListView_InsertItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // index
      m_strbuf.SetFormatted(128, "%d", i + 1);
      lvi.pszText = (char *)m_strbuf.Get();
      lvi.cchTextMax = m_strbuf.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // title
      lvi.pszText = (char *)ple->title.Get();
      lvi.cchTextMax = ple->title.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // artist
      lvi.pszText = (char *)ple->artist.Get();
      lvi.cchTextMax = ple->artist.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // album
      lvi.pszText = (char *)ple->album.Get();
      lvi.cchTextMax = ple->album.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // length
      int minutes = 0;
      int seconds = 0;
      double time = 0.0;
      time = ple->length;
      if (WDL_DefinitelyGreaterThan(time, 0.0))
      {
        minutes = (int)time / 60;
        seconds = (int)time % 60;
      }
      m_strbuf.SetFormatted(128, "%01d:%02d", minutes, seconds);
      lvi.pszText = (char *)m_strbuf.Get();
      lvi.cchTextMax = m_strbuf.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // genre
      lvi.pszText = (char *)ple->genre.Get();
      lvi.cchTextMax = ple->genre.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // samplerate
      m_strbuf.SetFormatted(128,
        "%.0f Hz", ple->srate);
      lvi.pszText = (char *)m_strbuf.Get();
      lvi.cchTextMax = m_strbuf.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // filename
      lvi.pszText = (char *)ple->filename.Get();
      lvi.cchTextMax = ple->filename.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // filepath
      lvi.pszText = (char *)ple->filepath.Get();
      lvi.cchTextMax = ple->filepath.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // fileext
      lvi.pszText = (char *)ple->fileext.Get();
      lvi.cchTextMax = ple->fileext.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // filesize
      double mb = ((double)ple->filesize / 1024.0 / 1024.0);
      m_strbuf.SetFormatted(128, "%.2f MB", mb);
      lvi.pszText = (char *)m_strbuf.Get();
      lvi.cchTextMax = m_strbuf.GetLength();
      ListView_SetItem(m_list, &lvi);

      lvi.mask = LVIF_TEXT;
      lvi.iSubItem = si++; // comment
      lvi.pszText = (char *)ple->comment.Get();
      lvi.cchTextMax = ple->comment.GetLength();
      ListView_SetItem(m_list, &lvi);
    }

    m_selstart = pos;
    m_selend = m_selstart + g_playlist->GetPasteSize();

    ApplySelection(); UpdateIndexes();
    SendMessage(m_list, WM_SETREDRAW, TRUE, 0);

    m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes pasted",
      m_titleorig.Get(), g_playlist->GetClipboardBytes());
    SetTitle(m_strbuf.Get());
  }
}

void THA_MainWnd::DatabaseCopy()
{
  HTREEITEM it = TreeView_GetSelection(m_tree);
  if (!it) return;

  TVITEM tvi;
  tvi.hItem = it;
  tvi.mask = TVIF_TEXT | TVIF_CHILDREN | TVIF_PARAM;
  char text[2048];
  char lparam[2048];
  tvi.pszText = (char *)text;
  tvi.lParam = (LPARAM)lparam;
  tvi.cchTextMax = sizeof(text);
  TreeView_GetItem(m_tree, &tvi);

  WDL_PtrList<WDL_FastString> fn;

  if (!strcmp((const char *)tvi.lParam, "root"))
  {
    HTREEITEM artist = TreeView_GetChild(m_tree, it);
    HTREEITEM artist_sibling = artist;
    while (artist_sibling)
    {
      HTREEITEM album_sibling = TreeView_GetChild(m_tree, artist_sibling);
      while(album_sibling)
      {
        HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
        while(title_sibling)
        {
          tvi.hItem = title_sibling;
          TreeView_GetItem(m_tree, &tvi);
          fn.Add(new WDL_FastString((const char *)tvi.lParam));
          title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
        }
        album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
      }
      artist_sibling = TreeView_GetNextSibling(m_tree, artist_sibling);
    }
  }
  else if (!strcmp((const char *)tvi.lParam, "artist"))
  {
    HTREEITEM album = TreeView_GetChild(m_tree, it);
    HTREEITEM album_sibling = album;
    while(album_sibling)
    {
      HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
      while(title_sibling)
      {
        tvi.hItem = title_sibling;
        TreeView_GetItem(m_tree, &tvi);
        fn.Add(new WDL_FastString((const char *)tvi.lParam));
        title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
      }
      album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
    }
  }
  else if (!strcmp((const char *)tvi.lParam, "album"))
  {
    HTREEITEM title = TreeView_GetChild(m_tree, it);
    HTREEITEM title_sibling = title;
    while(title_sibling)
    {
      tvi.hItem = title_sibling;
      TreeView_GetItem(m_tree, &tvi);
      fn.Add(new WDL_FastString((const char *)tvi.lParam));
      title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
    }
  }
  else
  {
    tvi.hItem = it;
    TreeView_GetItem(m_tree, &tvi);
    fn.Add(new WDL_FastString((const char *)tvi.lParam));
  }

  g_playlist->CopyFilepaths(&fn);

  WDL_INT64 sz = 0;
  for (int i = 0; i < fn.GetSize(); i++)
  {
    sz += fn.Get(i)->GetLength();
  }
  m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
    m_titleorig.Get(), sz);
  SetTitle(m_strbuf.Get());

  fn.Empty(true);
}

void THA_MainWnd::DatabaseOpenFileLocation()
{
  HTREEITEM it = TreeView_GetSelection(m_tree);
  if (!it) return;

  TVITEM tvi;
  tvi.hItem = it;
  tvi.mask = TVIF_TEXT | TVIF_CHILDREN | TVIF_PARAM;
  char text[2048];
  char lparam[2048];
  tvi.pszText = (char *)text;
  tvi.lParam = (LPARAM)lparam;
  tvi.cchTextMax = sizeof(text);
  TreeView_GetItem(m_tree, &tvi);

  WDL_PtrList_DeleteOnDestroy<char> fn(free);

  if (!strcmp((const char *)tvi.lParam, "root"))
  {
    HTREEITEM artist = TreeView_GetChild(m_tree, it);
    HTREEITEM artist_sibling = artist;
    while (artist_sibling)
    {
      HTREEITEM album_sibling = TreeView_GetChild(m_tree, artist_sibling);
      while(album_sibling)
      {
        HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
        while(title_sibling)
        {
          tvi.hItem = title_sibling;
          TreeView_GetItem(m_tree, &tvi);
          fn.Add(strdup((const char *)tvi.lParam)); break;
          title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
        }
        album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
      }
      artist_sibling = TreeView_GetNextSibling(m_tree, artist_sibling);
    }
  }
  else if (!strcmp((const char *)tvi.lParam, "artist"))
  {
    HTREEITEM album = TreeView_GetChild(m_tree, it);
    HTREEITEM album_sibling = album;
    while(album_sibling)
    {
      HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
      while(title_sibling)
      {
        tvi.hItem = title_sibling;
        TreeView_GetItem(m_tree, &tvi);
        fn.Add(strdup((const char *)tvi.lParam)); break;
        title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
      }
      album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
    }
  }
  else if (!strcmp((const char *)tvi.lParam, "album"))
  {
    HTREEITEM title = TreeView_GetChild(m_tree, it);
    HTREEITEM title_sibling = title;
    while(title_sibling)
    {
      tvi.hItem = title_sibling;
      TreeView_GetItem(m_tree, &tvi);
      fn.Add(strdup((const char *)tvi.lParam)); break;
      title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
    }
  }
  else
  {
    tvi.hItem = it;
    TreeView_GetItem(m_tree, &tvi);
    fn.Add(strdup((const char *)tvi.lParam));
  }

  if (fn.GetSize())
  {
    WDL_FastString tmp(fn.Get(0)); tmp.remove_filepart();
    ShellExecute(m_hwnd, "", "explorer.exe", tmp.Get(), "", SW_SHOWNORMAL);
  }
}

void THA_MainWnd::StartPlayback()
{
  m_activesel = ListView_GetNextItem(m_list, -1, LVNI_SELECTED|LVNI_FOCUSED);
  if (m_activesel >= 0)
  {
    int subitem = 0;
    char text[2048];
    ColumnHeader *ch = m_column_header.Get();

    for (int i = 0; i < m_column_header.GetSize(); i++)
    {
      if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
      {
        subitem = i;
        break;
      }
    }

    StopPlayback();

    do
    {
      THA_Track *newtrk = new WDL_NEW THA_Track;
      if (newtrk)
      {
        ListView_GetItemText(m_list, m_activesel,
          subitem, text, sizeof(text));
        if (newtrk->Open(text))
        {
          g_rendersystem->AddTrack(newtrk);
          g_playlist->SetActiveEntry(m_activesel);
          InvalidateRect(m_list, NULL, TRUE);

          if (!g_audiostreamer->IsRunning())
          {
            g_audiostreamer->Start(THA_AudioOnSamples);
          }
          return;
        } else delete newtrk;
      }
    } while (m_activesel++ < ListView_GetItemCount(m_list));
  }
}

void THA_MainWnd::StopPlayback()
{
  if (g_audiostreamer->IsRunning())
  {
    g_audiostreamer->Stop();
  }

  g_playlist->SetActiveEntry(-1);
  g_rendersystem->DeleteAllTracks();
  InvalidateRect(m_list, NULL, TRUE);
}

void THA_MainWnd::FocusTrack()
{
  if (ListView_GetItemCount(m_list)) SetFocus(m_list);
  if (m_activesel >= 0)
  {
    for (int i = 0; i < ListView_GetItemCount(m_list); i++)
    {
      ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
      ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
    }
    ListView_EnsureVisible(m_list, m_activesel, FALSE);
    ListView_SetItemState(m_list, m_activesel, LVNI_SELECTED|LVNI_FOCUSED, LVNI_SELECTED|LVNI_FOCUSED);

    int subitem = 0;
    char text[2048];
    ColumnHeader *ch = m_column_header.Get();

    for (int i = 0; i < m_column_header.GetSize(); i++)
    {
      if (ch[i].id == ID_LIBRARY_HEADER_TITLE)
      {
        subitem = i;
        break;
      }
    }

    ListView_GetItemText(m_list, m_activesel, subitem, text, sizeof(text));
    m_strbuf.SetFormatted(512, "%s | %s", m_titleorig.Get(), text);
    SetTitle(m_strbuf.Get());
  }
  else
  {
    int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED|LVNI_FOCUSED);
    if (pos >= 0)
    {
      for (int i = 0; i < ListView_GetItemCount(m_list); i++)
      {
        ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
        ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
      }
      ListView_EnsureVisible(m_list, pos, FALSE);
      ListView_SetItemState(m_list, pos, LVNI_SELECTED|LVNI_FOCUSED, LVNI_SELECTED|LVNI_FOCUSED);
    }
  }
}

void THA_MainWnd::FocusPlayList()
{
  if (ListView_GetItemCount(m_list)) SetFocus(m_list);
  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED|LVNI_FOCUSED);
  if (pos >= 0)
  {
    for (int i = 0; i < ListView_GetItemCount(m_list); i++)
    {
      ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
      ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
    }
    ListView_EnsureVisible(m_list, pos, FALSE);
    ListView_SetItemState(m_list, pos, LVNI_SELECTED|LVNI_FOCUSED, LVNI_SELECTED|LVNI_FOCUSED);
  }
  else
  {
    for (int i = 0; i < ListView_GetItemCount(m_list); i++)
    {
      ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
      ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
    }
    ListView_EnsureVisible(m_list, 0, FALSE);
    ListView_SetItemState(m_list, 0, LVNI_SELECTED|LVNI_FOCUSED, LVNI_SELECTED|LVNI_FOCUSED);
  }
}

void THA_MainWnd::Refresh()
{
  Search();
  g_playlist->Reload();
}

void THA_MainWnd::ToggleTime()
{
  m_timestate = !m_timestate;
}

void THA_MainWnd::RemoveMissingFiles()
{
  int subitem = 0;
  char text[2048];
  ColumnHeader *ch = m_column_header.Get();

  for (int i = 0; i < m_column_header.GetSize(); i++)
  {
    if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
    {
      subitem = i;
      break;
    }
  }

  SendMessage(m_list, WM_SETREDRAW, FALSE, 0);

  for (int i = 0, rem = 0; i < ListView_GetItemCount(m_list);)
  {
    ListView_GetItemText(m_list, i, subitem, text, sizeof(text));
    if (strncmp(text, "http", 4))
    {
      WDL_FileRead f(text);
      if (!f.IsOpen())
      {
        g_playlist->RemoveIndex(i);
        ListView_DeleteItem(m_list, i);
        rem++;
        m_strbuf.SetFormatted(512, "%s | %d/%d files, %d removed",
          m_titleorig.Get(), i + 1, ListView_GetItemCount(m_list), rem);
        SetTitle(m_strbuf.Get());
      }
      else
      {
        m_strbuf.SetFormatted(512, "%s | %d/%d files, %d removed",
          m_titleorig.Get(), i + 1, ListView_GetItemCount(m_list), rem);
        SetTitle(m_strbuf.Get());
        i++;
      }
    }
    else
    {
      m_strbuf.SetFormatted(512, "%s | %d/%d files, %d removed",
        m_titleorig.Get(), i + 1, ListView_GetItemCount(m_list), rem);
      SetTitle(m_strbuf.Get());
      i++;
    }
  }

  m_selstart = 0;
  m_selend = m_selstart + 1;

  ApplySelection(); UpdateIndexes();
  SendMessage(m_list, WM_SETREDRAW, TRUE, 0);
}

void THA_MainWnd::RemoveDuplicateFiles()
{
  int subitem = 0;
  char texta[4096];
  char textb[4096];
  ColumnHeader *ch = m_column_header.Get();

  for (int i = 0; i < m_column_header.GetSize(); i++)
  {
    if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
    {
      subitem = i;
      break;
    }
  }

  SendMessage(m_list, WM_SETREDRAW, FALSE, 0);

  for (int i = 0, rem = 0; i < ListView_GetItemCount(m_list); i++)
  {
    ListView_GetItemText(m_list, i, subitem, texta, sizeof(texta));

    for (int j = ListView_GetItemCount(m_list) - 1; j > i; j--)
    {
      ListView_GetItemText(m_list, j, subitem, textb, sizeof(textb));
      if (!wdl_filename_cmp(texta, textb))
      {
        g_playlist->RemoveIndex(j);
        ListView_DeleteItem(m_list, j);
        rem++;
      }
    }
    m_strbuf.SetFormatted(512, "%s | %d/%d files, %d removed",
      m_titleorig.Get(), i + 1, ListView_GetItemCount(m_list), rem);
    SetTitle(m_strbuf.Get());
  }

  m_selstart = 0;
  m_selend = m_selstart + 1;

  ApplySelection(); UpdateIndexes();
  SendMessage(m_list, WM_SETREDRAW, TRUE, 0);
}

void THA_MainWnd::PlayListFind(bool enable)
{
  m_playlistfind = enable;
}

bool THA_MainWnd::IsPlayListFind() const
{
  return m_playlistfind;
}

void THA_MainWnd::Find()
{
  m_editlabel->SetText("PL:");

  char text[2048];
  WDL_String str;
  WDL_FastString sql;
  GetWindowText(m_edit, text, sizeof(text));

  str.Set(text);
  WDL_TypedBuf<int> qp;

  for (int j = 0; j < str.GetLength(); j++)
  {
    char *quote = str.Get();

    if (quote[j] == '\'')
    {
      // 1 2 3 4 5 6 7 8 9 10
      //         '          '
      int pos = j + qp.GetSize();
      qp.Add(pos);
    }
  }

  for (int k = 0; k < qp.GetSize(); k++)
  {
    str.Insert("'", qp.Get()[k]);
  }

  bool whitespace = true;
  for (int i = 0; i < str.GetLength(); i++)
  {
    if (str.Get()[i] == ' ') continue; else { whitespace = false; break; }
  }

  for (int i = 0; i < ListView_GetItemCount(m_list); i++)
  {
    ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
    ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
  }

  if (!whitespace)
  {
    double wall0 = time_precise();
    g_query->QueryForPlayList(text);
    double wall1 = time_precise();
    m_strbuf.SetFormatted(512, "%s | PL query in %f sec",
      m_titleorig.Get(), wall1 - wall0);
    SetTitle(m_strbuf.Get());
  }
}

void THA_MainWnd::GotoIndex()
{
  if (!g_indexwnd)
  {
    CreateDialog(g_inst, MAKEINTRESOURCE(IDD_GOTO_INDEX), m_hwnd, THA_GotoIndexWndProc);
  }
  else
  {
    SetFocus(g_indexwnd);
  }
}

void THA_MainWnd::GotoIndex(int index)
{
  int sz = ListView_GetItemCount(m_list);
  int idx = wdl_clamp(index, 0, sz - 1);

  for (int i = 0; i < sz; i++)
  {
    ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
    ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
  }

  ListView_SetItemState(m_list, idx, LVNI_SELECTED|LVNI_FOCUSED, LVNI_SELECTED|LVNI_FOCUSED);
  ListView_EnsureVisible(m_list, idx, FALSE);
  SetFocus(m_list);
}

void THA_MainWnd::MoveToIndex()
{
  if (!g_movewnd)
  {
    CreateDialog(g_inst, MAKEINTRESOURCE(IDD_MOVE_TO_INDEX), m_hwnd, THA_MoveToIndexWndProc);
  }
  else
  {
    SetFocus(g_movewnd);
  }
}

void THA_MainWnd::MoveToIndex(int index)
{
  WDL_TypedBuf<int> selected;

  int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
  while (pos != -1)
  {
    selected.Add(pos);
    pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
  }

  m_selstart = index;
  if (selected.GetSize())
  {
    if (m_selstart > selected.Get()[0])
    {
      m_selstart -= selected.GetSize() - 1;
    }
  }
  m_selend = m_selstart + selected.GetSize();

  g_playlist->MoveSelection(&selected, index);
}

void THA_MainWnd::HideEditLabel()
{
  if (m_editlabel->IsVisible())
  {
    char text[2048];
    int len = GetWindowText(m_edit, text, sizeof(text));
    if (!len)
    {
      m_editlabel->SetVisible(false);
      m_vwnd.RequestRedraw(NULL);
    }
  }
}

void THA_MainWnd::ShowEditLabel()
{
  if (!m_editlabel->IsVisible())
  {
    m_editlabel->SetVisible(true);
    m_vwnd.RequestRedraw(NULL);
  }
}

void THA_MainWnd::SetInfo(const char *info)
{
  m_info->SetText(info);
  SetTimer(m_hwnd, THA_CLEAR_INFO, THA_CLEAR_INFO_MS, NULL);
}

void THA_MainWnd::SetTitle(const char *title)
{
  SetWindowText(m_hwnd, title);
  SetTimer(m_hwnd, THA_ORIGINAL_TITLE, THA_ORIGINAL_TITLE_MS, NULL);
}

const char *THA_MainWnd::GetOrigTitle() const
{
  return m_titleorig.Get();
}

void THA_MainWnd::AddURL(const char *url)
{
  double wall0 = time_precise();
  m_selstart = ListView_GetItemCount(m_list);
  m_selend = m_selstart + 1;
  if (g_playlist->AddURL(url))
  {
    double wall1 = time_precise();
    m_strbuf.SetFormatted(512, "%s | URL added in %f sec",
      m_titleorig.Get(), wall1 - wall0);
    SetTitle(m_strbuf.Get());
  }
}

void THA_MainWnd::SetSelection(int start, int end)
{
  m_selstart = start;
  m_selend = end;
}

void THA_MainWnd::Metadata()
{
  if (!g_metadatawnd)
  {
    char text[2048];
    int subitem = 0;
    ColumnHeader *ch = m_column_header.Get();

    for (int i = 0; i < m_column_header.GetSize(); i++)
    {
      if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
      {
        subitem = i;
        break;
      }
    }

    if (g_metadataqueue->HasFilesInQueue())
    {
      MessageBox(m_hwnd, "Metadata has items in queue for processing. Try again later.", "Metadata", MB_OK);
      return;
    }

    int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
    while (pos != -1)
    {
      ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));
      g_metadataqueue->AddFile(text);
      pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
    }

    if (g_metadataqueue->GetFilePathCount())
    {
      g_metadatawnd = new THA_MetadataWnd;
      CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_METADATA), m_hwnd,
        THA_MetadataWnd::MetadataWndProc, (LPARAM)g_metadatawnd);
      ShowWindow(g_metadatawnd->Handle(), SW_SHOW);
    }
    else
    {
      MessageBox(m_hwnd, "Metadata is not available for stream.", "Metadata", MB_OK);
    }
  }
  else
  {
    SetFocus(g_metadatawnd->Handle());
  }
}

void THA_MainWnd::Artwork()
{
  int nit = 0;
  int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
  while (pos != -1)
  {
    nit++;
    if (nit > 1) return;
    pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
  }

  if (!g_artworkwnd)
  {
    char text[2048];
    int subitem = 0;
    ColumnHeader *ch = m_column_header.Get();

    for (int i = 0; i < m_column_header.GetSize(); i++)
    {
      if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
      {
        subitem = i;
        break;
      }
    }

    int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED | LVIS_FOCUSED);
    if (pos >= 0)
    {
      ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));
    }

    if (strncmp(text, "http", 4))
    {
      THA_IFilePic *pic = CreateFilePic(text);
      if (pic && pic->GetPicSize())
      {
        WDL_ASSERT(!g_artworkbm);
        g_artworkbm = LICE_LoadJPGFromMemory(pic->GetPic(), pic->GetPicSize());
        if (!g_artworkbm)
        {
          g_artworkbm = LICE_LoadPNGFromMemory(pic->GetPic(), pic->GetPicSize());
        }

        delete pic;

        if (g_artworkbm)
        {
          g_artworkwnd = new THA_ArtworkWnd;
          CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_ARTWORK), m_hwnd,
            THA_ArtworkWnd::ArtworkWndProc, (LPARAM)g_artworkwnd);
          ShowWindow(g_artworkwnd->Handle(), SW_SHOW);
        }
        else
        {
          MessageBox(m_hwnd, "No artwork found.", "Artwork", MB_OK);
          return;
        }
      }
      else
      {
        MessageBox(m_hwnd, "No artwork found.", "Artwork", MB_OK);
        return;
      }
    }
    else
    {
      MessageBox(m_hwnd, "Artwork is not available for stream.", "Artwork", MB_OK);
    }
  }
  else
  {
    SetFocus(g_artworkwnd->Handle());
  }
}

void THA_MainWnd::FileInfo()
{
  int nit = 0;
  int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
  while (pos != -1)
  {
    nit++;
    if (nit > 1) return;
    pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
  }

  if (!g_fileinfownd)
  {
    char text[2048];
    int subitem = 0;
    ColumnHeader *ch = m_column_header.Get();

    for (int i = 0; i < m_column_header.GetSize(); i++)
    {
      if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
      {
        subitem = i;
        break;
      }
    }

    int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED | LVIS_FOCUSED);
    if (pos >= 0)
    {
      ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));
    }

    if (strncmp(text, "http", 4))
    {
      g_fileinfownd = new THA_FileInfoWnd;
      CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_FILEINFO), m_hwnd,
        THA_FileInfoWnd::FileInfoWndProc, (LPARAM)g_fileinfownd);
      if (g_fileinfownd) ShowWindow(g_fileinfownd->Handle(), SW_SHOW);
    }
    else
    {
      MessageBox(m_hwnd, "File info is not available for stream.", "File info", MB_OK);
    }
  }
  else
  {
    SetFocus(g_fileinfownd->Handle());
  }
}

int THA_MainWnd::GetHeaderSubitem(int id) const
{
  int subitem = 0;
  ColumnHeader *ch = m_column_header.Get();

  for (int i = 0; i < m_column_header.GetSize(); i++)
  {
    if (ch[i].id == id) // ID_LIBRARY_HEADER_FILEPATH
    {
      subitem = i;
      break;
    }
  }

  return subitem;
}

void THA_MainWnd::ApplySelection()
{
  for (int i = 0; i < ListView_GetItemCount(m_list); i++)
  {
    ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
    ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
    if (i == m_selstart) ListView_SetItemState(m_list, i, LVIS_FOCUSED, LVIS_FOCUSED);
    if (i >= m_selstart && i < m_selend)
    {
      ListView_SetItemState(m_list, i, LVIS_SELECTED, LVIS_SELECTED);
      if (i == m_selend - 1) m_selstart = m_selend = -1;
    }
  }

  for (int i = 0; i < m_sel.GetSize(); i++)
  {
    if (!i) ListView_SetItemState(m_list, m_sel.Get()[i], LVIS_FOCUSED, LVIS_FOCUSED);
    ListView_SetItemState(m_list, m_sel.Get()[i], LVIS_SELECTED, LVIS_SELECTED);
  }
}

void THA_MainWnd::UpdateIndexes()
{
  int subitem = 0;
  ColumnHeader *ch = m_column_header.Get();

  for (int i = 0; i < m_column_header.GetSize(); i++)
  {
    if (ch[i].id == ID_LIBRARY_HEADER_INDEX)
    {
      subitem = i;
      break;
    }
  }

  for (int i = 0; i < ListView_GetItemCount(m_list); i++)
  {
    LVITEM lvi = { 0, };
    int si = 0;
    lvi.mask = LVIF_TEXT | LVIF_PARAM;
    lvi.lParam = (LPARAM)i;
    lvi.iItem = i;
    lvi.iSubItem = subitem; // index
    m_strbuf.SetFormatted(128, "%d", i + 1);
    lvi.pszText = (char *)m_strbuf.Get();
    lvi.cchTextMax = m_strbuf.GetLength();
    ListView_SetItem(m_list, &lvi);
    ListView_SetItemText(m_list, i, subitem,
      (char *)m_strbuf.Get());
  }
}

void THA_MainWnd::ScanFiles(const char *path, bool empty_file_cache)
{
  WDL_DirScan dir;
  WDL_FastString fn;

  m_lp.parse(g_media_ext.Get());

  if (empty_file_cache)
  {
    m_file_cache.Empty(true);
  }

  if (!dir.First(path))
  {
    do
    {
      m_strbuf.SetFormatted(512, "%s | Scanning directories...",
        m_titleorig.Get());
      SetTitle(m_strbuf.Get());

      if (dir.GetCurrentIsDirectory())
      {
        dir.GetCurrentFullFN(&fn);

        if (strcmp(fn.get_filepart(), ".") &&
            strcmp(fn.get_filepart(), ".."))
        {
          ScanFiles(fn.Get(), false);
        }
      }
      else
      {
        WDL_FastString *str = new WDL_FastString();

        if (str)
        {
          dir.GetCurrentFullFN(str);

          bool valid_ext = false;

          for (int i = 0; i < m_lp.getnumtokens(); i++)
          {
            if (wdl_filename_cmp(str->get_fileext(), m_lp.gettoken_str(i)) == 0)
            {
              valid_ext = true;
              break;
            }
          }

          if (valid_ext)
          {
            m_file_cache.Add(str);
          }
          else
          {
            delete str;
          }
        }
        else
        {
          delete str;
        }
      }
    }
    while (!dir.Next());
  }

  qsort(m_file_cache.GetList(), m_file_cache.GetSize(),
    sizeof(WDL_FastString *), sort_by_file_path);
}

void THA_MainWnd::OnInitDialog(WPARAM wparam, LPARAM lparam)
{
  double wall0 = time_precise();

  if (IsStableRelease())
  {
    m_titleorig.Set(THA_NAME_MARKETING);
  }
  else
  {
    m_titleorig.Set(THA_FULL_VERSION " (" THA_ARCH ")");
  }
  SetWindowText(m_hwnd, m_titleorig.Get());

  m_font = new LICE_CachedFont();
  m_font->SetFromHFont(CreateFontIndirect(&lf), LICE_FONT_FLAG_OWNS_HFONT);

  m_vwnd.SetRealParent(m_hwnd);
  m_time = new WDL_VirtualStaticText;
  m_info = new WDL_VirtualStaticText;
  m_editlabel = new WDL_VirtualStaticText;

  RECT timrec;
  timrec.left = 0 + 8;
  timrec.top = 0 + 8;
  timrec.right = timrec.left + 50;
  timrec.bottom = timrec.top + 15;
  m_time->SetFont(m_font);
  m_time->SetID(0);
  m_time->SetPosition(&timrec);
  m_time->SetRealParent(m_hwnd);
  //m_time.SetBorder(true);
  m_time->SetAlign(-1);
  m_time->SetText("");
  m_vwnd.AddChild(m_time);

  RECT infrec;
  infrec.left = timrec.right;
  infrec.top = 0 + 8;
  infrec.right = infrec.left + 50;
  infrec.bottom = infrec.top + 15;
  m_info->SetFont(m_font);
  m_info->SetID(1);
  m_info->SetPosition(&infrec);
  m_info->SetRealParent(m_hwnd);
  //m_time.SetBorder(true);
  m_info->SetAlign(1);
  m_info->SetText("");
  m_vwnd.AddChild(m_info);

  RECT sts;
  GetWindowRect(GetDlgItem(m_hwnd, IDC_EDIT1), &sts);
  ScreenToClient(m_hwnd, (LPPOINT)&sts);
  ScreenToClient(m_hwnd, ((LPPOINT)&sts)+1);
  sts.left = 0 + 8;
  sts.right = sts.left + 16;
  sts.top = sts.top - 3;
  sts.bottom = sts.bottom - 3;
  m_editlabel->SetFont(m_font);
  m_editlabel->SetID(2);
  m_editlabel->SetPosition(&sts);
  m_editlabel->SetRealParent(m_hwnd);
  //m_editlabel.SetBorder(true);
  m_editlabel->SetAlign(-1);
  m_editlabel->SetText("DB:");
  m_vwnd.AddChild(m_editlabel);
  m_vwnd.RequestRedraw(NULL);

  m_resize.init(m_hwnd);
  m_resize.init_item(IDC_EDIT1, 0.0f, 0.0f, 0.0f, 0.0f);
  m_resize.init_item(IDC_TREE1, 0.0f, 0.0f, 0.0f, 1.0f);
  m_resize.init_item(IDC_LIST1, 0.0f, 0.0f, 1.0f, 1.0f);
  m_resize.init_itemvirt(m_vwnd.GetChildByID(0), 0.0f, 0.0f, 0.0f, 0.0f);
  m_resize.init_itemvirt(m_vwnd.GetChildByID(1), 0.0f, 0.0f, 0.0f, 0.0f);

  m_main_menu = LoadMenu(g_inst, MAKEINTRESOURCE(IDR_MAIN_MENUBAR));
  SetMenu(m_hwnd, m_main_menu);

  m_x = GetPrivateProfileInt(THA_NAME, "main_wnd_x", 50, g_inipath.Get());
  m_y = GetPrivateProfileInt(THA_NAME, "main_wnd_y", 50, g_inipath.Get());
  m_w = GetPrivateProfileInt(THA_NAME, "main_wnd_w", 1120, g_inipath.Get());
  m_h = GetPrivateProfileInt(THA_NAME, "main_wnd_h", 900, g_inipath.Get());
  SetWindowPos(m_hwnd, NULL, m_x, m_y, m_w, m_h, SWP_NOACTIVATE);

  int edit_x, edit_y, edit_w, edit_h;
  WDL_WndSizer__rec *recedit = m_resize.get_item(IDC_EDIT1);
  edit_x = recedit->orig.left;
  edit_y = recedit->orig.top;
  edit_w = recedit->orig.right - recedit->orig.left;
  edit_h = recedit->orig.bottom - recedit->orig.top;
  edit_x = GetPrivateProfileInt(THA_NAME, "edit_x", edit_x, g_inipath.Get());
  edit_y = GetPrivateProfileInt(THA_NAME, "edit_y", edit_y, g_inipath.Get());
  edit_w = GetPrivateProfileInt(THA_NAME, "edit_w", edit_w, g_inipath.Get());
  edit_h = GetPrivateProfileInt(THA_NAME, "edit_h", edit_h, g_inipath.Get());
  recedit->orig.right = edit_x + edit_w;

  WDL_WndSizer__rec *rectime = m_resize.get_itembyvirt(m_vwnd.GetChildByID(0));
  rectime->orig.right = edit_x + (edit_w / 2);
  WDL_WndSizer__rec *recinfo = m_resize.get_itembyvirt(m_vwnd.GetChildByID(1));
  recinfo->orig.left = edit_x + (edit_w / 2);
  recinfo->orig.right = edit_x + edit_w;

  int tree_x, tree_y, tree_w, tree_h;
  WDL_WndSizer__rec *rectree = m_resize.get_item(IDC_TREE1);
  tree_x = rectree->orig.left;
  tree_y = rectree->orig.top;
  tree_w = rectree->orig.right - rectree->orig.left;
  tree_h = rectree->orig.bottom - rectree->orig.top;
  tree_x = GetPrivateProfileInt(THA_NAME, "tree_x", tree_x, g_inipath.Get());
  tree_y = GetPrivateProfileInt(THA_NAME, "tree_y", tree_y, g_inipath.Get());
  tree_w = GetPrivateProfileInt(THA_NAME, "tree_w", tree_w, g_inipath.Get());
  tree_h = GetPrivateProfileInt(THA_NAME, "tree_h", tree_h, g_inipath.Get());
  rectree->orig.right = tree_x + tree_w;

  int list_x, list_y, list_w, list_h;
  WDL_WndSizer__rec *reclist = m_resize.get_item(IDC_LIST1);
  list_x = reclist->orig.left;
  list_y = reclist->orig.top;
  list_w = reclist->orig.right - reclist->orig.left;
  list_h = reclist->orig.bottom - reclist->orig.top;
  list_x = GetPrivateProfileInt(THA_NAME, "list_x", list_x, g_inipath.Get());
  list_y = GetPrivateProfileInt(THA_NAME, "list_y", list_y, g_inipath.Get());
  list_w = GetPrivateProfileInt(THA_NAME, "list_w", list_w, g_inipath.Get());
  list_h = GetPrivateProfileInt(THA_NAME, "list_h", list_h, g_inipath.Get());
  reclist->orig.left = list_x;
  m_resize.onResize();

  RECT r;
  GetClientRect(m_hwnd, &r);

  int lrm = 2, tbm = 2;
  int tx = 0, ty = 0, tw = ((r.right - lrm) / 4) - lrm, th = 0;

  int maximized = GetPrivateProfileInt(THA_NAME, "main_wnd_maximized", 0, g_inipath.Get());

  if (maximized)
  {
    ShowWindow(m_hwnd, SW_SHOWMAXIMIZED);
  }
  else
  {
    ShowWindow(m_hwnd, SW_SHOWNORMAL);
  }

  m_selstart = 0;
  m_selend = m_selstart + 1;
  g_metadataqueue = new THA_MetadataQueue;
  g_playlist = new THA_PlayList;
  g_database = new THA_Database;
  g_database->Open();

  g_query = new THA_Query;
  g_query->Open();
  g_query->Query();

  m_tree = GetDlgItem(m_hwnd, IDC_TREE1);
  WDL_UTF8_HookTreeView(m_tree);

  //TreeView_SetExtendedStyle(m_tree, TVS_EX_DOUBLEBUFFER, TVS_EX_DOUBLEBUFFER);

  m_strbuf.Set("All music [0]");

  m_tvinsert.hParent = NULL;
  m_tvinsert.hInsertAfter = TVI_LAST;
  m_tvinsert.item.mask = TVIF_TEXT|TVIF_PARAM;
  m_tvinsert.item.pszText = (char *)m_strbuf.Get();
  m_tvinsert.item.cchTextMax = m_strbuf.GetLength();
  m_tvinsert.item.lParam = 0;

  TreeView_InsertItem(m_tree, &m_tvinsert);

  m_list = GetDlgItem(m_hwnd, IDC_LIST1);
  WDL_UTF8_HookListView(m_list);

#if defined(_WIN32)
  ListView_SetExtendedListViewStyleEx(m_list,
    LVS_EX_HEADERDRAGDROP | LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER,
    LVS_EX_HEADERDRAGDROP | LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER);
#else
  ListView_SetExtendedListViewStyleEx(m_list,
    LVS_EX_HEADERDRAGDROP | LVS_EX_FULLROWSELECT,
    LVS_EX_HEADERDRAGDROP | LVS_EX_FULLROWSELECT);
#endif

  ColumnHeader default_column_header[] =
  {
    // The alignment of the leftmost column is
    // always LVCFMT_LEFT; it cannot be changed.
    { ID_LIBRARY_HEADER_TYPE, "Type", 50, LVCFMT_LEFT, 1, 0 },
    { ID_LIBRARY_HEADER_INDEX, "#", 30, LVCFMT_RIGHT, 1, 1 },
    { ID_LIBRARY_HEADER_TITLE, "Title", 200, LVCFMT_LEFT, 1, 0 },
    { ID_LIBRARY_HEADER_ARTIST, "Artist", 200, LVCFMT_LEFT, 1, 0 },
    { ID_LIBRARY_HEADER_ALBUM, "Album", 200, LVCFMT_LEFT, 1, 0 },
    { ID_LIBRARY_HEADER_LENGTH, "Length", 50, LVCFMT_RIGHT, 1, 1 },
    { ID_LIBRARY_HEADER_GENRE, "Genre", 80, LVCFMT_LEFT, 1, 0 },
    { ID_LIBRARY_HEADER_SAMPLERATE, "Sample rate", 80, LVCFMT_RIGHT, 1, 1 },
    { ID_LIBRARY_HEADER_FILENAME, "File name", 150, LVCFMT_LEFT, 1, 0 },
    { ID_LIBRARY_HEADER_FILEPATH, "File path", 150, LVCFMT_LEFT, 1 , 0 },
    { ID_LIBRARY_HEADER_FILEEXTENSION, "File extension", 80, LVCFMT_LEFT, 1, 0 },
    { ID_LIBRARY_HEADER_FILESIZE, "File size", 100, LVCFMT_RIGHT, 1, 1 },
    { ID_LIBRARY_HEADER_COMMENT, "Comment", 100, LVCFMT_LEFT, 1, 0 }
  };

  m_column_header.Resize(sizeof(default_column_header) /
    sizeof(default_column_header[0]));

  for (int i = 0; i < m_column_header.GetSize(); i++)
  {
    ColumnHeader *ch = m_column_header.Get();
    ch[i] = default_column_header[i];
  }

  LVCOLUMN lvc = { 0, };
  lvc.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;

  int w = 0;
  WDL_FastString s;

  ColumnHeader *ch = m_column_header.Get();

  for (int i = 0; i < m_column_header.GetSize(); i++)
  {
    s.SetFormatted(128, "library_column_width_%02d", i);
    w = GetPrivateProfileInt(THA_NAME, s.Get(), ch[i].width, g_inipath.Get());
    lvc.cx = w;
    lvc.fmt = ch[i].fmt;
    lvc.pszText = (char *)ch[i].text;
    ListView_InsertColumn(m_list, i, &lvc);
  }

  WDL_TypedBuf<int> order_array;
  order_array.Resize(Header_GetItemCount(ListView_GetHeader(m_list)));
  int *oa = order_array.Get();

  for (int i = 0; i < order_array.GetSize(); i++)
  {
    s.SetFormatted(128, "library_column_order_%02d", i);
    oa[i] = GetPrivateProfileInt(THA_NAME, s.Get(), i, g_inipath.Get());
  }

  ListView_SetColumnOrderArray(m_list, order_array.GetSize(), order_array.Get());

  int defcol = GetPrivateProfileInt(THA_NAME, "library_defcol", 1, g_inipath.Get());
  if (defcol)
  {
    SendMessage(m_hwnd, WM_COMMAND, ID_LIBRARY_HEADER_DEFAULT, 0);
  }

  m_database_menu = (HMENU)GetSubMenu(LoadMenu(g_inst, MAKEINTRESOURCE(IDR_LIBRARY_CONTEXT)), 0);
  m_column_menu = (HMENU)GetSubMenu(LoadMenu(g_inst, MAKEINTRESOURCE(IDR_LIBRARY_CONTEXT)), 1);
  m_playlist_menu = (HMENU)GetSubMenu(LoadMenu(g_inst, MAKEINTRESOURCE(IDR_LIBRARY_CONTEXT)), 2);
  m_time_menu = (HMENU)GetSubMenu(LoadMenu(g_inst, MAKEINTRESOURCE(IDR_LIBRARY_CONTEXT)), 3);

  m_edit = GetDlgItem(m_hwnd, IDC_EDIT1);

  PrevTreeProc = (WNDPROC)SetWindowLongPtrW(GetDlgItem(m_hwnd, IDC_TREE1),
  GWLP_WNDPROC, (LONG_PTR)NewTreeProc);
  s_tree = m_tree;

  PrevListProc = (WNDPROC)SetWindowLongPtrW(GetDlgItem(m_hwnd, IDC_LIST1),
  GWLP_WNDPROC, (LONG_PTR)NewListProc);
  s_list = m_list;

  PrevEditProc = (WNDPROC)SetWindowLongPtrW(GetDlgItem(m_hwnd, IDC_EDIT1),
    GWLP_WNDPROC, (LONG_PTR)NewEditProc);
  s_edit = m_edit;


  int bk = g_preferences->GetBackgroundColor();
  int fg = g_preferences->GetTextColor();
  ListView_SetBkColor(m_list, RGB((bk >> 16) & 0xFF,(bk >> 8) & 0xFF, bk & 0xFF));
  ListView_SetTextBkColor(m_list, RGB((bk >> 16) & 0xFF,(bk >> 8) & 0xFF, bk & 0xFF));
  ListView_SetTextColor(m_list, RGB((fg >> 16) & 0xFF,(fg >> 8) & 0xFF, fg & 0xFF));

  bk = g_preferences->GetBackgroundColor();
  fg = g_preferences->GetTextColor();
  TreeView_SetBkColor(m_tree, RGB((bk >> 16) & 0xFF,(bk >> 8) & 0xFF, bk & 0xFF));
  TreeView_SetTextColor(m_tree, RGB((fg >> 16) & 0xFF,(fg >> 8) & 0xFF, fg & 0xFF));

  g_rendersystem = new THA_RenderSystem;
  g_audiostreamer = CreateAudioStreamer();

  if (g_rendersystem && g_audiostreamer)
  {
    if (!g_audiostreamer->Open())
    {
      int fmt = g_preferences->GetAudioDeviceBitDepth();
      int srate = (int)g_preferences->GetAudioDeviceSamplerate();
      int nch = g_preferences->GetAudioDeviceOutputChannels();

      WDL_FastString fmtstr;

      switch (fmt)
      {
      case 0: fmtstr.Set("16-bit"); break;
      case 1: fmtstr.Set("24-bit"); break;
      case 2: fmtstr.Set("32-bit"); break;
      case 3: fmtstr.Set("32-bit FP"); break;
      default: fmtstr.Set("Unknown"); break;
      }

      WDL_FastString err;
      err.SetFormatted(1024,
        THA_NAME_MARKETING " cannot open the audio device with the following settings:\n\n"
        "Audio bit depth: %s\n"
        "Audio sample rate: %d\n"
        "Audio output channels: %d\n\n"
        "Make sure that the related settings are correct in " THA_NAME_MARKETING " preferences.",
        fmtstr.Get(), srate, nch);

      MessageBox(m_hwnd, err.Get(), "Audio device error", MB_OK);
    }
    else
    {
      WDL_ASSERT(!g_rendersystem->IsRunning());
      WDL_ASSERT(!g_audiostreamer->IsRunning());
      g_rendersystem->StartThread();
    }
  }
  else
  {
    MessageBox(m_hwnd,
      THA_NAME_MARKETING " cannot find a valid audio system.\n"
      "Press OK to close " THA_NAME_MARKETING ".",
      "Audio system error", MB_OK);
    DestroyWindow(m_hwnd);
  }

  char vol[512];
  GetPrivateProfileString(THA_NAME, "volume", "0.0", vol, sizeof(vol), g_inipath.Get());
  g_rendersystem->SetVolume(atof(vol));

  double wall1 = time_precise();

  if (WDL_unlikely(!IsStableRelease()))
  {
    const char warn_msg[] =
    {
      "Welcome to " THA_NAME_MARKETING " (the \"Software\").\n\n"
      "THIS IS AN UNSTABLE RELEASE STILL IN DEVELOPEMENT PHASE\n"
      "AND IS PROVIDED ON AN \"AS IS\" BASIS AND IS BELIEVED\n"
      "TO CONTAIN DEFECTS AND A PRIMARY PURPOSE OF THIS\n"
      "RELEASE IS TO OBTAIN FEEDBACK ON SOFTWARE PERFORMANCE\n"
      "AND THE IDENTIFICATION OF DEFECTS.\n\n"
      "IT IS ADVISED TO USE CAUTION AND NOT TO RELY IN ANY WAY\n"
      "ON THE CORRECT FUNCTIONING OF THIS RELEASE.\n\n"
      THA_NAME_MARKETING " version: " THA_NAKED_VERSION " (" THA_ARCH ")\n\n"
      "For the latest production release visit: " THA_WEBSITE_URL "\n\n"
      "Startup time: %.3f sec\n\n"
      THA_COPYRIGHT
    };

    WDL_FastString tmp;

    tmp.SetFormatted(1024, warn_msg, wall1 - wall0);

    MessageBox(m_hwnd, tmp.Get(), "Release for testing purposes", MB_OK);
  }

  m_strbuf.SetFormatted(512, "%s | Startup time: %.3f sec",
    m_titleorig.Get(), wall1 - wall0);
  SetTitle(m_strbuf.Get());

  if (g_playlist->GetPlayList()->GetSize())
  {
    SetFocus(m_list);
  }
  else
  {
    SetFocus(m_tree);
  }

  m_marquee = GetPrivateProfileInt(THA_NAME, "marquee", -1, g_inipath.Get());
  SetTimer(m_hwnd, THA_MAIN_SCREEN_UPDATE, THA_MAIN_SCREEN_UPDATE_MS, NULL);
}

void THA_MainWnd::OnSysCommand(WPARAM wparam, LPARAM lparam)
{
  if (wparam == SC_CLOSE)
  {
    bool active = false;

    //if (active && g_preferences->AskExit())
    if (0)
    {
      int res = MessageBox(m_hwnd, "Playback is active. Do you really want to Exit?", "Ask Exit", MB_YESNO);
      if (res == IDYES) DestroyWindow(m_hwnd);
    }
    else
    {
      DestroyWindow(m_hwnd);
    }
  }
}

INT_PTR THA_MainWnd::OnClose(WPARAM wparam, LPARAM lparam)
{
  return 0;
}

void THA_MainWnd::OnSize(WPARAM wparam, LPARAM lparam)
{
  if (wparam != SIZE_MINIMIZED)
  {
    m_titlecapture = false;
    m_resize.onResize();
  }

  //InvalidateRect(m_hwnd, NULL, FALSE);

  if (wparam != SIZE_MINIMIZED && wparam != SIZE_MAXIMIZED)
  {
    RECT r;
    GetWindowRect(m_hwnd, &r);

    m_x = r.left;
    m_y = r.top;
    m_w = r.right - r.left;
    m_h = r.bottom - r.top;
  }
}

void THA_MainWnd::OnMove(WPARAM wparam, LPARAM lparam)
{
  int xpos = (int)(short) LOWORD(lparam); // horizontal position
  int ypos = (int)(short) HIWORD(lparam); // vertical position

  if (xpos >= 0 && ypos >= 0 && GetCapture() == m_hwnd)
  {
    RECT r;
    GetWindowRect(m_hwnd, &r);

    m_x = r.left;
    m_y = r.top;
    m_w = r.right - r.left;
    m_h = r.bottom - r.top;
  }
}

void THA_MainWnd::OnDestroy(WPARAM wparam, LPARAM lparam)
{
  KillTimer(m_hwnd, THA_MAIN_SCREEN_UPDATE);

  WDL_FastString vol;
  vol.SetFormatted(32, "%f", g_rendersystem->GetVolume());
  WritePrivateProfileString(THA_NAME, "volume", vol.Get(), g_inipath.Get());

#ifdef _WIN32
  WINDOWPLACEMENT wp = { sizeof(wp) };
  GetWindowPlacement(m_hwnd, &wp);

  WDL_FastString xstr, ystr, wstr, hstr;
  xstr.SetFormatted(32, "%d", m_x);
  ystr.SetFormatted(32, "%d", m_y);
  wstr.SetFormatted(32, "%d", m_w);
  hstr.SetFormatted(32, "%d", m_h);

  if (wp.showCmd == SW_SHOWMAXIMIZED)
  {
    WritePrivateProfileString(THA_NAME, "main_wnd_maximized", "1", g_inipath.Get());
  }
  else
  {
    WritePrivateProfileString(THA_NAME, "main_wnd_maximized", "0", g_inipath.Get());
    WritePrivateProfileString(THA_NAME, "main_wnd_x", xstr.Get(), g_inipath.Get());
    WritePrivateProfileString(THA_NAME, "main_wnd_y", ystr.Get(), g_inipath.Get());
    WritePrivateProfileString(THA_NAME, "main_wnd_w", wstr.Get(), g_inipath.Get());
    WritePrivateProfileString(THA_NAME, "main_wnd_h", hstr.Get(), g_inipath.Get());
  }

  RECT r;
  GetWindowRect(m_edit, &r);
  ScreenToClient(m_hwnd, (LPPOINT)&r);
  ScreenToClient(m_hwnd, ((LPPOINT)&r)+1);
  xstr.SetFormatted(32, "%d", r.left);
  ystr.SetFormatted(32, "%d", r.top);
  wstr.SetFormatted(32, "%d", r.right - r.left);
  hstr.SetFormatted(32, "%d", r.bottom - r.top);
  WritePrivateProfileString(THA_NAME, "edit_x", xstr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "edit_y", ystr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "edit_w", wstr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "edit_h", hstr.Get(), g_inipath.Get());

  GetWindowRect(m_tree, &r);
  ScreenToClient(m_hwnd, (LPPOINT)&r);
  ScreenToClient(m_hwnd, ((LPPOINT)&r)+1);
  xstr.SetFormatted(32, "%d", r.left);
  ystr.SetFormatted(32, "%d", r.top);
  wstr.SetFormatted(32, "%d", r.right - r.left);
  hstr.SetFormatted(32, "%d", r.bottom - r.top);
  WritePrivateProfileString(THA_NAME, "tree_x", xstr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "tree_y", ystr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "tree_w", wstr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "tree_h", hstr.Get(), g_inipath.Get());

  GetWindowRect(m_list, &r);
  ScreenToClient(m_hwnd, (LPPOINT)&r);
  ScreenToClient(m_hwnd, ((LPPOINT)&r)+1);
  xstr.SetFormatted(32, "%d", r.left);
  ystr.SetFormatted(32, "%d", r.top);
  wstr.SetFormatted(32, "%d", r.right - r.left);
  hstr.SetFormatted(32, "%d", r.bottom - r.top);
  WritePrivateProfileString(THA_NAME, "list_x", xstr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "list_y", ystr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "list_w", wstr.Get(), g_inipath.Get());
  WritePrivateProfileString(THA_NAME, "list_h", hstr.Get(), g_inipath.Get());

  WDL_FastString s;
  WDL_TypedBuf<int> order_array;
  WDL_FastString str;

  order_array.Resize(Header_GetItemCount(ListView_GetHeader(m_list)));
  ListView_GetColumnOrderArray(m_list, order_array.GetSize(), order_array.Get());

  int *oa = order_array.Get();

  for (int i = 0; i < order_array.GetSize(); i++)
  {
    s.SetFormatted(128, "library_column_order_%02d", i);
    str.SetFormatted(32, "%d", oa[i]);
    WritePrivateProfileString(THA_NAME, s.Get(), str.Get(), g_inipath.Get());
  }

  for (int i = 0; i < Header_GetItemCount(ListView_GetHeader(m_list)); i++)
  {
    s.SetFormatted(128, "library_column_width_%02d", i);
    str.SetFormatted(32, "%d", ListView_GetColumnWidth(m_list, i));
    WritePrivateProfileString(THA_NAME, s.Get(), str.Get(), g_inipath.Get());
  }

  int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED|LVIS_FOCUSED);
  str.SetFormatted(32, "%d", pos);
  WritePrivateProfileString(THA_NAME, "marquee", str.Get(), g_inipath.Get());

#else
  RECT r;
  GetWindowRect(m_hwnd, &r);

  WDL_FastString xstr, ystr, wstr, hstr;
  xstr.SetFormatted(32, "%d", r.left);
  ystr.SetFormatted(32, "%d", r.top);
  wstr.SetFormatted(32, "%d", r.right - r.left);
  hstr.SetFormatted(32, "%d", r.bottom - r.top);

  if (r.left >= 0 && r.top >= 0)
  {
    WritePrivateProfileString(THA_NAME, "main_wnd_x", xstr.Get(), g_inipath.Get());
    WritePrivateProfileString(THA_NAME, "main_wnd_y", ystr.Get(), g_inipath.Get());
    WritePrivateProfileString(THA_NAME, "main_wnd_w", wstr.Get(), g_inipath.Get());
    WritePrivateProfileString(THA_NAME, "main_wnd_h", hstr.Get(), g_inipath.Get());
  }

  #ifdef __APPLE__
  SWELL_PostQuitMessage(g_mainwnd);
  #endif
  g_hasrequestedquit = true;
#endif

  m_vwnd.RemoveAllChildren();

  if (g_audiostreamer->IsRunning())
  {
    StopPlayback();
    g_audiostreamer->Close();
  }

  if (g_rendersystem->IsRunning())
  {
    g_rendersystem->StopThread();
  }

  delete g_metadataqueue;
  delete g_audiostreamer;
  delete g_rendersystem;
  delete g_database;
  delete g_query;
  delete g_playlist;

  PostQuitMessage(0);
}

void THA_MainWnd::OnCommand(WPARAM wparam, LPARAM lparam)
{
  switch (LOWORD(wparam))
  {
  //case IDOK:
  //  {
  //    SetDlgItemText(m_hwnd, IDC_EDIT1, "");
  //  }
  //  break;
    case ID_LIBRARY_HEADER_DEFAULT:
      {
        int defzero[] =
        {
          ID_LIBRARY_HEADER_TYPE,
          ID_LIBRARY_HEADER_GENRE,
          ID_LIBRARY_HEADER_FILENAME,
          ID_LIBRARY_HEADER_FILEPATH,
          ID_LIBRARY_HEADER_FILEEXTENSION,
          ID_LIBRARY_HEADER_COMMENT
        };

        int defstd[] =
        {
          ID_LIBRARY_HEADER_INDEX,
          ID_LIBRARY_HEADER_TITLE,
          ID_LIBRARY_HEADER_ARTIST,
          ID_LIBRARY_HEADER_ALBUM,
          ID_LIBRARY_HEADER_LENGTH,
          ID_LIBRARY_HEADER_SAMPLERATE,
          ID_LIBRARY_HEADER_FILESIZE
        };

        for (int i = 0, it = 0; i < (sizeof(defzero) / sizeof(defzero[0])); i++)
        {
          for (int j = 0; j < m_column_header.GetSize(); j++)
          {
            if (m_column_header.Get()[j].id == defzero[i])
            {
              it = j;
              break;
            }
          }

          ListView_SetColumnWidth(m_list, it, 0);
        }

        for (int i = 0, it = 0, w = 0; i < (sizeof(defstd) / sizeof(defstd[0])); i++)
        {
          for (int j = 0; j < m_column_header.GetSize(); j++)
          {
            if (m_column_header.Get()[j].id == defstd[i])
            {
              it = j;
              w = m_column_header.Get()[j].width;
              break;
            }
          }

          ListView_SetColumnWidth(m_list, it, w);
        }
      }
      break;
    case ID_DATABASE_ADDTOPLAYLIST:
      {
        HTREEITEM it = TreeView_GetSelection(m_tree);
        if (!it) return;

        TVITEM tvi;
        tvi.hItem = it;
        tvi.mask = TVIF_TEXT | TVIF_CHILDREN | TVIF_PARAM;
        char text[2048];
        char lparam[2048];
        tvi.pszText = (char *)text;
        tvi.lParam = (LPARAM)lparam;
        tvi.cchTextMax = sizeof(text);
        TreeView_GetItem(m_tree, &tvi);

        double wall0 = time_precise();
        WDL_PtrList_DeleteOnDestroy<char> fn(free);

        if (!strcmp((const char *)tvi.lParam, "root"))
        {
          HTREEITEM artist = TreeView_GetChild(m_tree, it);
          HTREEITEM artist_sibling = artist;
          while (artist_sibling)
          {
            HTREEITEM album_sibling = TreeView_GetChild(m_tree, artist_sibling);
            while(album_sibling)
            {
              HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
              while(title_sibling)
              {
                tvi.hItem = title_sibling;
                TreeView_GetItem(m_tree, &tvi);
                fn.Add(strdup((const char *)tvi.lParam));
                title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
              }
              album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
            }
            artist_sibling = TreeView_GetNextSibling(m_tree, artist_sibling);
          }
        }
        else if (!strcmp((const char *)tvi.lParam, "artist"))
        {
          HTREEITEM album = TreeView_GetChild(m_tree, it);
          HTREEITEM album_sibling = album;
          while(album_sibling)
          {
            HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
            while(title_sibling)
            {
              tvi.hItem = title_sibling;
              TreeView_GetItem(m_tree, &tvi);
              fn.Add(strdup((const char *)tvi.lParam));
              title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
            }
            album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
          }
        }
        else if (!strcmp((const char *)tvi.lParam, "album"))
        {
          HTREEITEM title = TreeView_GetChild(m_tree, it);
          HTREEITEM title_sibling = title;
          while(title_sibling)
          {
            tvi.hItem = title_sibling;
            TreeView_GetItem(m_tree, &tvi);
            fn.Add(strdup((const char *)tvi.lParam));
            title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
          }
        }
        else
        {
          tvi.hItem = it;
          TreeView_GetItem(m_tree, &tvi);
          fn.Add(strdup((const char *)tvi.lParam));
        }

        if (fn.GetSize())
        {
          m_selstart = ListView_GetItemCount(m_list);
          m_selend = m_selstart + fn.GetSize();
          for (int i = 0; i < fn.GetSize(); i++)
          {
            g_playlist->AddFilepath(fn.Get(i));
          }
          double wall1 = time_precise();
          if (fn.GetSize() == 1)
          {
            m_strbuf.SetFormatted(512, "%s | %d file added in %f sec",
              m_titleorig.Get(), fn.GetSize(), wall1 - wall0);
            SetTitle(m_strbuf.Get());
          }
          else
          {
            m_strbuf.SetFormatted(512, "%s | %d files added in %f sec",
              m_titleorig.Get(), fn.GetSize(), wall1 - wall0);
            SetTitle(m_strbuf.Get());
          }
        }
      } break;
    case ID_DATABASE_ADDTONEWPLAYLIST:
      {
        HTREEITEM it = TreeView_GetSelection(m_tree);
        if (!it) return;

        TVITEM tvi;
        tvi.hItem = it;
        tvi.mask = TVIF_TEXT | TVIF_CHILDREN | TVIF_PARAM;
        char text[2048];
        char lparam[2048];
        tvi.pszText = (char *)text;
        tvi.lParam = (LPARAM)lparam;
        tvi.cchTextMax = sizeof(text);
        TreeView_GetItem(m_tree, &tvi);

        double wall0 = time_precise();
        WDL_PtrList_DeleteOnDestroy<char> fn(free);

        if (!strcmp((const char *)tvi.lParam, "root"))
        {
          HTREEITEM artist = TreeView_GetChild(m_tree, it);
          HTREEITEM artist_sibling = artist;
          while (artist_sibling)
          {
            HTREEITEM album_sibling = TreeView_GetChild(m_tree, artist_sibling);
            while(album_sibling)
            {
              HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
              while(title_sibling)
              {
                tvi.hItem = title_sibling;
                TreeView_GetItem(m_tree, &tvi);
                fn.Add(strdup((const char *)tvi.lParam));
                title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
              }
              album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
            }
            artist_sibling = TreeView_GetNextSibling(m_tree, artist_sibling);
          }
        }
        else if (!strcmp((const char *)tvi.lParam, "artist"))
        {
          HTREEITEM album = TreeView_GetChild(m_tree, it);
          HTREEITEM album_sibling = album;
          while(album_sibling)
          {
            HTREEITEM title_sibling = TreeView_GetChild(m_tree, album_sibling);
            while(title_sibling)
            {
              tvi.hItem = title_sibling;
              TreeView_GetItem(m_tree, &tvi);
              fn.Add(strdup((const char *)tvi.lParam));
              title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
            }
            album_sibling = TreeView_GetNextSibling(m_tree, album_sibling);
          }
        }
        else if (!strcmp((const char *)tvi.lParam, "album"))
        {
          HTREEITEM title = TreeView_GetChild(m_tree, it);
          HTREEITEM title_sibling = title;
          while(title_sibling)
          {
            tvi.hItem = title_sibling;
            TreeView_GetItem(m_tree, &tvi);
            fn.Add(strdup((const char *)tvi.lParam));
            title_sibling = TreeView_GetNextSibling(m_tree, title_sibling);
          }
        }
        else
        {
          tvi.hItem = it;
          TreeView_GetItem(m_tree, &tvi);
          fn.Add(strdup((const char *)tvi.lParam));
        }

        if (fn.GetSize())
        {
          ListView_DeleteAllItems(m_list);
          g_playlist->DeleteAllFilepaths();
          m_selstart = ListView_GetItemCount(m_list);
          m_selend = m_selstart + fn.GetSize();
          for (int i = 0; i < fn.GetSize(); i++)
          {
            g_playlist->AddFilepath(fn.Get(i));
          }
          double wall1 = time_precise();
          if (fn.GetSize() == 1)
          {
            m_strbuf.SetFormatted(512, "%s | %d file added in %f sec",
              m_titleorig.Get(), fn.GetSize(), wall1 - wall0);
            SetTitle(m_strbuf.Get());
          }
          else
          {
            m_strbuf.SetFormatted(512, "%s | %d files added in %f sec",
              m_titleorig.Get(), fn.GetSize(), wall1 - wall0);
            SetTitle(m_strbuf.Get());
          }
        }
      } break;
    case ID_DATABASE_COPY:
      {
        DatabaseCopy();
      } break;
    case ID_DATABASE_OPENFILELOCATION:
      {
        DatabaseOpenFileLocation();
      } break;
    case ID_LIBRARY_HEADER_TYPE:
    case ID_LIBRARY_HEADER_TITLE:
    case ID_LIBRARY_HEADER_ARTIST:
    case ID_LIBRARY_HEADER_ALBUM:
    case ID_LIBRARY_HEADER_LENGTH:
    case ID_LIBRARY_HEADER_GENRE:
    case ID_LIBRARY_HEADER_SAMPLERATE:
    case ID_LIBRARY_HEADER_FILENAME:
    case ID_LIBRARY_HEADER_FILEPATH:
    case ID_LIBRARY_HEADER_FILEEXTENSION:
    case ID_LIBRARY_HEADER_FILESIZE:
    case ID_LIBRARY_HEADER_INDEX:
    case ID_LIBRARY_HEADER_COMMENT:
      for (int i = 0; i < m_column_header.GetSize(); i++)
      {
        ColumnHeader *ch = m_column_header.Get();

        if (ch[i].id == LOWORD(wparam))
        {
          MENUITEMINFO mii = { 0 };
          mii.cbSize = sizeof(mii);
          mii.fMask = MIIM_STATE;

          if (GetMenuItemInfo(m_column_menu, ch[i].id, FALSE, &mii))
          {
            if (!(mii.fState & MFS_CHECKED))
            {
              ListView_SetColumnWidth(m_list, i, ch[i].width);
            }
            else if (mii.fState & MFS_CHECKED)
            {
              ListView_SetColumnWidth(m_list, i, 0);
            }

            //mii.fState ^= MFS_CHECKED;
            //SetMenuItemInfo(m_column_menu, pl_column_info[i].id, FALSE, &mii);
            WDL_FastString str;
            str.SetFormatted(32, "%d", 0);
            WritePrivateProfileString(THA_NAME, "library_defcol", str.Get(), g_inipath.Get());
          }
        }
      }
      break;
  case ID_PLAYLIST_PLAY:
    {
      StartPlayback();
    } break;
  case ID_PLAYLIST_OPENFILELOCATION:
    {
      OpenFileLocation();
    } break;
  case ID_COPYPROPERTY_TYPE:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_TYPE)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_TITLE:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_TITLE)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_ARTIST:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_ARTIST)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_ALBUM:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_ALBUM)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_LENGTH:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_LENGTH)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_GENRE:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_GENRE)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_SAMPLERATE:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_SAMPLERATE)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_FILENAME:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_FILENAME)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_FILEPATH:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_FILEEXTENSION:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_FILEEXTENSION)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_FILESIZE:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_FILESIZE)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_COPYPROPERTY_COMMENT:
    {
      if (OpenClipboard(m_hwnd))
      {
        int nit = 0;
        int pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        while (pos != -1)
        {
          nit++;
          if (nit > 1) return;
          pos = ListView_GetNextItem(m_list, pos, LVIS_SELECTED);
        }

        pos = ListView_GetNextItem(m_list, -1, LVIS_SELECTED);
        if (pos >= 0)
        {
          int subitem = 0;
          char text[2048];
          ColumnHeader *ch = m_column_header.Get();

          for (int i = 0; i < m_column_header.GetSize(); i++)
          {
            if (ch[i].id == ID_LIBRARY_HEADER_COMMENT)
            {
              subitem = i;
              break;
            }
          }

          ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

          EmptyClipboard();

          WCHAR wide[4096];
          WDL_MBtoWideStr(wide, text, sizeof(wide));
          int len = (int)wcslen(wide);

          HGLOBAL buf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (len + 1));
          if (buf)
          {
            WCHAR *cptr = (WCHAR *)GlobalLock(buf);
            memcpy(cptr, wide, len * sizeof(WCHAR));
            cptr[len] = 0;
            GlobalUnlock(buf);
            SetClipboardData(CF_UNICODETEXT, buf);

            m_strbuf.SetFormatted(512, "%s | %" WDL_PRI_INT64 " bytes copied",
              m_titleorig.Get(), (WDL_INT64)(len * sizeof(WCHAR)));
            SetTitle(m_strbuf.Get());
          }
          CloseClipboard();
        }
      }
    } break;
  case ID_PLAYLIST_LENGTH:
    {
      WDL_TypedBuf<int> selected;

      int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
      while (pos != -1)
      {
        selected.Add(pos);
        pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
      }

      if (selected.GetSize())
      {
        int hours = 0, minutes = 0, seconds = 0;
        double time = g_playlist->GetLength(&selected);
        double mb = g_playlist->GetSizeMB(&selected);
        if (WDL_DefinitelyGreaterThan(time, 0.0))
        {
          minutes = (int)time / 60;
          seconds = (int)time % 60;
          hours = minutes / 60;
          minutes = minutes % 60;
        }
        if (selected.GetSize() == 1)
        {
          m_strbuf.SetFormatted(512, "%01d:%02d:%02d, %d file, %.2f MB",
            hours, minutes, seconds, selected.GetSize(), mb);
        }
        else
        {
          m_strbuf.SetFormatted(512, "%01d:%02d:%02d, %d files, %.2f MB",
            hours, minutes, seconds, selected.GetSize(), mb);
        }
        MessageBox(m_hwnd, m_strbuf.Get(), "Length", MB_OK);
      }
    } break;
  case ID_PLAYLIST_METADATA:
    {
      Metadata();
    } break;
  case ID_PLAYLIST_ARTWORK:
    {
      Artwork();
    } break;
  case ID_PLAYLIST_FILEINFO:
    {
      FileInfo();
    } break;
  case ID_TIME_TIMEELAPSED:
    {
      m_timestate = false;
    } break;
  case ID_TIME_TIMEREMAINING:
    {
      m_timestate = true;
    } break;
  case ID_TIME_STOP:
    {
      StopPlayback();
    } break;
  case ID_SORT_REVERSE:
    {
      ReverseSelected();
    } break;
  case ID_SORT_RANDOM:
    {
      RandomizeSelected();
    } break;
  case ID_SORT_TYPE:
    {
      Sort(0, false, true);
    } break;
  case ID_SORT_TITLE:
    {
      Sort(2, false, true);
    } break;
  case ID_SORT_ARTIST:
    {
      Sort(3, false, true);
    } break;
  case ID_SORT_ALBUM:
    {
      Sort(4, false, true);
    } break;
  case ID_SORT_LENGTH:
    {
      Sort(5, false, true);
    } break;
  case ID_SORT_GENRE:
    {
      Sort(6, false, true);
    } break;
  case ID_SORT_SAMPLERATE:
    {
      Sort(7, false, true);
    } break;
  case ID_SORT_FILENAME:
    {
      Sort(8, false, true);
    } break;
  case ID_SORT_FILEPATH:
    {
      Sort(9, false, true);
    } break;
  case ID_SORT_FILEEXTENSION:
    {
      Sort(10, false, true);
    } break;
  case ID_SORT_FILESIZE:
    {
      Sort(11, false, true);
    } break;
  case ID_SORT_COMMENT:
    {
      Sort(12, false, true);
    } break;
  case ID_SORT_TYPEREVERSE:
    {
      Sort(0, true, true);
    } break;
  case ID_SORT_TITLEREVERSE:
    {
      Sort(2, true, true);
    } break;
  case ID_SORT_ARTISTREVERSE:
    {
      Sort(3, true, true);
    } break;
  case ID_SORT_ALBUMREVERSE:
    {
      Sort(4, true, true);
    } break;
  case ID_SORT_LENGTHREVERSE:
    {
      Sort(5, true, true);
    } break;
  case ID_SORT_GENREREVERSE:
    {
      Sort(6, true, true);
    } break;
  case ID_SORT_SAMPLERATEREVERSE:
    {
      Sort(7, true, true);
    } break;
  case ID_SORT_FILENAMEREVERSE:
    {
      Sort(8, true, true);
    } break;
  case ID_SORT_FILEPATHREVERSE:
    {
      Sort(9, true, true);
    } break;
  case ID_SORT_FILEEXTENSIONREVERSE:
    {
      Sort(10, true, true);
    } break;
  case ID_SORT_FILESIZEREVERSE:
    {
      Sort(11, true, true);
    } break;
  case ID_SORT_COMMENTREVERSE:
    {
      Sort(12, true, true);
    } break;
  case ID_PLAYLIST_REMOVE:
    {
      RemoveSelected();
    } break;
  case ID_PLAYLIST_CROP:
    {
      CropSelected();
    } break;
  case ID_PLAYLIST_CUT:
    {
      Cut();
    } break;
  case ID_PLAYLIST_COPY:
    {
      Copy();
    } break;
  case ID_PLAYLIST_PASTE:
    {
      Paste();
    } break;
  case ID_FILE_ADDFILES:
    {
      bool preservecwd = g_preferences->WantPreserveDirectory();
      char *sel = WDL_ChooseFileForOpen(m_hwnd, "Add files", NULL, NULL,
        THA_OPEN_MEDIA_FILES, THA_OPEN_MEDIA_FILE_DEFEXT, preservecwd, true);

      if (sel)
      {
        double wall0 = time_precise();
        m_file_cache.Empty(true);

        const char *only_one = sel;

        while (*only_one++);

        if (*only_one == '\0' && *(only_one + 1) == '\0')
        {
          m_selstart = ListView_GetItemCount(m_list);
          m_selend = m_selstart + 1;
          if (g_playlist->AddFilepathDirectly(sel))
          {
            double wall1 = time_precise();
            m_strbuf.SetFormatted(512, "%s | 1 file added in %f sec",
              m_titleorig.Get(), wall1 - wall0);
            SetTitle(m_strbuf.Get());
          }
          free(sel);
        }
        else
        {
          WDL_FastString path;

          path.Set(sel);

          char *s = sel;
          while (*s++);

          do
          {
            WDL_FastString *fn = new WDL_FastString();

            if (fn)
            {
              fn->Set(path.Get());
              fn->Append(WDL_DIRCHAR_STR);
              fn->Append(s);
              m_file_cache.Add(fn);
            }

            while (*s++);
          } while (*s != '\0' && *(s + 1) != '\0');

          m_selstart = ListView_GetItemCount(m_list);
          m_selend = m_selstart + m_file_cache.GetSize();

          int cannotadd = 0;

          for (int i = 0; i < m_file_cache.GetSize(); i++)
          {
            WDL_FastString *str = m_file_cache.Get(i);
            if (g_playlist->AddFilepathDirectly(str->Get()))
            {
              m_strbuf.SetFormatted(512, "%s | %d/%d files",
                m_titleorig.Get(), i + 1, m_file_cache.GetSize());
              SetTitle(m_strbuf.Get());
            }
            else cannotadd++;
          }

          double wall1 = time_precise();
          m_strbuf.SetFormatted(512, "%s | %d/%d files added in %f sec",
            m_titleorig.Get(), m_file_cache.GetSize() - cannotadd,
            m_file_cache.GetSize(), wall1 - wall0);
          SetTitle(m_strbuf.Get());

          m_file_cache.Empty(true);
          free(sel);
        }
      }
    }
    break;
  case ID_FILE_ADDFOLDER:
    {
      char sel[4096];
      bool preservecwd = g_preferences->WantPreserveDirectory();

      if (WDL_ChooseDirectory(m_hwnd, "Add folder", NULL,
        sel, sizeof(sel), preservecwd))
      {
        double wall0 = time_precise();
        m_file_cache.Empty(true);

        ScanFiles(sel);

        m_selstart = ListView_GetItemCount(m_list);
        m_selend = m_selstart + m_file_cache.GetSize();

        int cannotadd = 0;

        for (int i = 0; i < m_file_cache.GetSize(); i++)
        {
          WDL_FastString *str = m_file_cache.Get(i);
          if (g_playlist->AddFilepathDirectly(str->Get()))
          {
            m_strbuf.SetFormatted(512, "%s | %d/%d files",
              m_titleorig.Get(), i + 1, m_file_cache.GetSize());
            SetTitle(m_strbuf.Get());
          }
          else cannotadd++;
        }

        double wall1 = time_precise();
        m_strbuf.SetFormatted(512, "%s | %d/%d files added in %f sec",
          m_titleorig.Get(), m_file_cache.GetSize() - cannotadd,
          m_file_cache.GetSize(), wall1 - wall0);
        SetTitle(m_strbuf.Get());

        m_file_cache.Empty(true);
      }
    }
    break;
  case ID_FILE_ADDURL:
    {
      if (!g_urlwnd)
      {
        CreateDialog(g_inst, MAKEINTRESOURCE(IDD_ADD_URL), m_hwnd, THA_AddURLWndProc);
      }
      else
      {
        SetFocus(g_urlwnd);
      }
    }
    break;
  case ID_FILE_SAVEPLAYLIST:
    {
      if (!g_saveplwnd)
      {
        g_saveplwnd = new THA_SavePlayListWnd;
        CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_SAVE_PLAYLIST), m_hwnd,
          THA_SavePlayListWnd::SavePlayListWndProc, (LPARAM)g_saveplwnd);
        ShowWindow(g_saveplwnd->Handle(), SW_SHOW);
      }
      else
      {
        SetFocus(g_saveplwnd->Handle());
      }
    } break;
  case ID_FILE_LOADPLAYLIST:
    {
      if (!g_loadplwnd)
      {
        g_loadplwnd = new THA_LoadPlayListWnd;
        CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_LOAD_PLAYLIST), m_hwnd,
          THA_LoadPlayListWnd::LoadPlayListWndProc, (LPARAM)g_loadplwnd);
        ShowWindow(g_loadplwnd->Handle(), SW_SHOW);
      }
      else
      {
        SetFocus(g_loadplwnd->Handle());
      }
    } break;
  case ID_FILE_EDITPLAYLISTS:
    {
      if (!g_editplwnd)
      {
        g_editplwnd = new THA_EditPlayListsWnd;
        CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_EDIT_PLAYLISTS), m_hwnd,
          THA_EditPlayListsWnd::EditPlayListsWndProc, (LPARAM)g_editplwnd);
        ShowWindow(g_editplwnd->Handle(), SW_SHOW);
      }
      else
      {
        SetFocus(g_editplwnd->Handle());
      }
    } break;
  case ID_FILE_EXIT:
    {
      DestroyWindow(g_mainwnd->Handle());
    }
    break;
  case ID_OPTIONS_SCANMEDIALIBRARYPATHS:
    {
      if (!g_database->IsScanInProgress())
      {
        g_database->ScanFiles();
      }
    }
    break;
  case ID_OPTIONS_PREFERENCES:
    {
      if (!g_preferences->WantCurses())
      {
        g_preferences->Open();
      }
      else
      {
        if (!g_prefwnd)
        {
          g_prefwnd = new THA_PreferencesWnd;
          CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_PREFERENCES), m_hwnd,
            THA_PreferencesWnd::PreferencesWndProc, (LPARAM)g_prefwnd);
          ShowWindow(g_prefwnd->Handle(), SW_SHOW);
        }
        else
        {
          SetFocus(g_prefwnd->Handle());
        }
      }
    }
    break;
  case ID_HELP_DOCUMENTATION:
    {
      WDL_FastString doc;
      doc.Set(g_modpath.Get());
      doc.Append("documentation.txt");

      ShellExecute(m_hwnd, "", "notepad.exe", doc.Get(), "", SW_SHOWNORMAL);
    }
    break;
  case ID_HELP_ABOUT:
    {
      if (!g_aboutwnd)
      {
        g_aboutwnd = new THA_AboutWnd;
        CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_ABOUT), m_hwnd,
          THA_AboutWnd::AboutWndProc, (LPARAM)g_aboutwnd);
        ShowWindow(g_aboutwnd->Handle(), SW_SHOW);
      }
      else
      {
        SetFocus(g_aboutwnd->Handle());
      }
    }
    break;
  case ID_HELP_CHANGELOG:
    {
      if (!g_changelogwnd)
      {
        g_changelogwnd = new THA_ChangelogWnd;
        CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_CHANGELOG), m_hwnd,
          THA_ChangelogWnd::ChangelogWndProc, (LPARAM)g_changelogwnd);
        ShowWindow(g_changelogwnd->Handle(), SW_SHOW);
      }
      else
      {
        SetFocus(g_changelogwnd->Handle());
      }
    }
    break;
  case ID_HELP_VERSION:
    {
      const char version_str[] =
      {
        THA_NAME_MARKETING " version: " THA_NAKED_VERSION " (" THA_ARCH ")\n\n"
        "Build timestamp: " THA_TIMESTAMP "\n"
        "Git commit: " THA_GIT_SHA
      };

      MessageBox(m_hwnd, version_str, "Version details", MB_OK);
    }
    break;
  case ID_HELP_OFFICIALWEBSITE:
    {
      ShellExecute(m_hwnd, "", THA_WEBSITE_URL, "", "", SW_SHOWNORMAL);
    }
    break;
  case ID_HELP_LICENSE:
    {
      if (!g_licwnd)
      {
        g_licwnd = new THA_LicenseWnd;
        CreateDialogParam(g_inst, MAKEINTRESOURCE(IDD_LICENSE), m_hwnd,
          THA_LicenseWnd::LicenseWndProc, (LPARAM)g_licwnd);
        ShowWindow(g_licwnd->Handle(), SW_SHOW);
      }
      else
      {
        SetFocus(g_licwnd->Handle());
      }
    }
    break;
  }
}

void THA_MainWnd::OnTimer(WPARAM wparam, LPARAM lparam)
{
  if (wparam == THA_CLEAR_EDIT)
  {
    SetWindowText(m_edit, "");
    KillTimer(m_hwnd, THA_CLEAR_EDIT);
  }

  if (wparam == THA_CLEAR_INFO)
  {
    m_info->SetText("");
    KillTimer(m_hwnd, THA_CLEAR_INFO);
  }

  if (wparam == THA_ORIGINAL_TITLE)
  {
    SetWindowText(m_hwnd, m_titleorig.Get());
    KillTimer(m_hwnd, THA_ORIGINAL_TITLE);
  }

  if (wparam == THA_DISABLE_CONTROLS)
  {
    if (!GetCapture() || !m_titlecapture)
    {
      KillTimer(m_hwnd, THA_DISABLE_CONTROLS);
      SendMessage(m_list, WM_SETREDRAW, TRUE, 0);
    }
  }

  if (wparam == THA_MAIN_SCREEN_UPDATE)
  {
    if (g_database->IsScanInProgress())
    {
      if (!m_scandb)
      {
        m_scandb = true;
        InvalidateRect(m_hwnd, NULL, FALSE);
        EnableMenuItem(GetMenu(m_hwnd), ID_OPTIONS_SCANMEDIALIBRARYPATHS, MF_GRAYED);
      }
      //else
      //{
      //  m_strbuf.SetFormatted(4096, "%s | %s",
      //    m_titleorig.Get(), g_database->GetProgressFilename());
      //  SetTitle(m_strbuf.Get());
      //}
    }
    else
    {
      if (m_scandb)
      {
        m_scandb = false; Search();
        InvalidateRect(m_hwnd, NULL, FALSE);
        EnableMenuItem(GetMenu(m_hwnd), ID_OPTIONS_SCANMEDIALIBRARYPATHS, MF_ENABLED);
      }
    }

    if (m_splitter)
    {
      WDL_WndSizer__rec *rectree = m_resize.get_itembywnd(m_tree);
      WDL_WndSizer__rec *reclist = m_resize.get_itembywnd(m_list);
      if (rectree && reclist)
      {
        int treew = rectree->last.right - rectree->last.left;
        int listw = reclist->last.right - reclist->last.left;
        m_strbuf.SetFormatted(512, "%s | DB: %d, PL: %d",
          m_titleorig.Get(), treew, listw);
        SetTitle(m_strbuf.Get());
      }
    }

    m_time->SetText(g_rendersystem->GetTime(m_timestate));

    THA_PlayList::ActiveEntry ae;
    g_playlist->GetActiveEntry(&ae);
    if (m_activesel != ae.pos)
    {
      m_activesel = ae.pos;
      if (g_preferences->WantTrackVisible())
      {
        ListView_EnsureVisible(m_list, m_activesel, FALSE);
      }
      InvalidateRect(m_list, NULL, TRUE);
    }

    if (g_rendersystem->IsStreaming())
    {
      int subitem = 0;
      char text[2048];
      ColumnHeader *ch = m_column_header.Get();

      for (int i = 0; i < m_column_header.GetSize(); i++)
      {
        if (ch[i].id == ID_LIBRARY_HEADER_TITLE)
        {
          subitem = i;
          break;
        }
      }

      ListView_GetItemText(m_list, m_activesel, subitem, text, sizeof(text));

      if (strcmp(g_rendersystem->GetStreamInfo(), text) &&
        strcmp(g_rendersystem->GetStreamInfo(), ""))
      {
        ListView_SetItemText(m_list, m_activesel, subitem,
          (char *)g_rendersystem->GetStreamInfo());
      }
    }

    if (g_query->WantArtists())
    {
      HTREEITEM parent = NULL;
      HTREEITEM artist = NULL;
      HTREEITEM album = NULL;
      HTREEITEM title = NULL;
      bool wantexpand = g_preferences->WantExpandedTree();

      THA_Query::Artist *ar = g_query->GetArtists();
      m_strbuf.SetFormatted(2048, "All music [%d]", ar->GetSize());

      TreeView_DeleteAllItems(m_tree);
      m_tvinsert.hParent = NULL;
      m_tvinsert.hInsertAfter = TVI_LAST;
      m_tvinsert.item.mask = TVIF_TEXT|TVIF_PARAM;
      m_tvinsert.item.pszText = (char *)m_strbuf.Get();
      m_tvinsert.item.cchTextMax = m_strbuf.GetLength();
      m_tvinsert.item.lParam = (LPARAM)"root";
      TreeView_InsertItem(m_tree, &m_tvinsert);
      parent = TreeView_GetChild(m_tree, TVI_ROOT);

      for (int i = 0; i < ar->GetSize(); i++)
      {
        const char *key;
        THA_Query::Album *al = ar->Enumerate(i, &key);

        if (WDL_likely(strlen(key) > 0))
        {
          m_strbuf.SetFormatted(2048, "%s [%d]", key, al->GetSize());
        }
        else m_strbuf.SetFormatted(2048, "[%d]", al->GetSize());

        m_tvinsert.hParent = parent;
        m_tvinsert.hInsertAfter = TVI_LAST;
        m_tvinsert.item.mask = TVIF_TEXT|TVIF_PARAM;
        m_tvinsert.item.pszText = (char *)m_strbuf.Get();
        m_tvinsert.item.cchTextMax = m_strbuf.GetLength();
        m_tvinsert.item.lParam = (LPARAM)"artist";
        artist = TreeView_InsertItem(m_tree, &m_tvinsert);

        for (int j = 0; j < al->GetSize(); j++)
        {
          const char *key;
          THA_Query::Title *ti = al->Enumerate(j, &key);

          if (WDL_likely(strlen(key) > 0))
          {
            m_strbuf.SetFormatted(2048, "%s [%d]", key, ti->GetSize());
          }
          else m_strbuf.SetFormatted(2048, "[%d]", ti->GetSize());

          m_tvinsert.hParent = artist;
          m_tvinsert.hInsertAfter = TVI_LAST;
          m_tvinsert.item.mask = TVIF_TEXT|TVIF_PARAM;
          m_tvinsert.item.pszText = (char *)m_strbuf.Get();
          m_tvinsert.item.cchTextMax = m_strbuf.GetLength();
          m_tvinsert.item.lParam = (LPARAM)"album";
          album = TreeView_InsertItem(m_tree, &m_tvinsert);

          for (int k = 0; k < ti->GetSize(); k++)
          {
            THA_Query::TitlePath *tp = ti->Get(k);

            if (WDL_unlikely(tp->title.GetLength() == 0))
            {
              tp->title.SetFormatted(2048, "%d", k + 1);
            }

            m_tvinsert.hParent = album;
            m_tvinsert.hInsertAfter = TVI_LAST;
            m_tvinsert.item.mask = TVIF_TEXT|TVIF_PARAM;
            m_tvinsert.item.pszText = (char *)tp->title.Get();
            m_tvinsert.item.cchTextMax = tp->title.GetLength();
            m_tvinsert.item.lParam = (LPARAM)tp->path.Get();
            title = TreeView_InsertItem(m_tree, &m_tvinsert);
          }
          if (wantexpand) TreeView_Expand(m_tree, artist, TVE_EXPAND);
          if (wantexpand) TreeView_Expand(m_tree, album, TVE_EXPAND);
        }
      }
      TreeView_Expand(m_tree, parent, TVE_EXPAND);
    }
    g_query->DoneWithArtists();

    if (g_query->WantMatches())
    {
      int first = 0xFFFFFF;

      int subitem = 0;
      char text[2048];
      ColumnHeader *ch = m_column_header.Get();

      for (int i = 0; i < m_column_header.GetSize(); i++)
      {
        if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
        {
          subitem = i;
          break;
        }
      }

      for (int i = 0; i < ListView_GetItemCount(m_list); i++)
      {
        ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
        ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
      }

      WDL_PtrList<WDL_FastString> *paths = g_query->GetMatches();

      for (int i = 0; i < paths->GetSize(); i++)
      {
        WDL_FastString *path = paths->Get(i);

        for (int j = 0; j < ListView_GetItemCount(m_list); j++)
        {
          ListView_GetItemText(m_list, j, subitem, text, sizeof(text));

          if (!wdl_filename_cmp(path->Get(), text))
          {
            first = wdl_min(first, j);
            ListView_SetItemState(m_list, j, LVIS_SELECTED, LVIS_SELECTED);
          }
        }
      }
      ListView_SetItemState(m_list, first, LVIS_FOCUSED, LVIS_FOCUSED);
      ListView_EnsureVisible(m_list, first, FALSE);
    }
    g_query->DoneWithMatches();

    if (g_playlist->WantPlayList())
    {
      WDL_PtrList<THA_PlayList::PlayListEntry> *entries = g_playlist->GetPlayList();

      ListView_DeleteAllItems(m_list);

      for (int i = 0; i < entries->GetSize(); i++)
      {
        THA_PlayList::PlayListEntry *ple = entries->Get(i);

        LVITEM lvi = { 0, };
        int si = 0;
        lvi.mask = LVIF_TEXT | LVIF_PARAM;
        lvi.lParam = (LPARAM)i;
        lvi.iItem = i;
        lvi.iSubItem = si++; // type
        lvi.pszText = (char *)ple->type.Get();
        lvi.cchTextMax = ple->type.GetLength();
        ListView_InsertItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // index
        m_strbuf.SetFormatted(128, "%d", i + 1);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // title
        lvi.pszText = (char *)ple->title.Get();
        lvi.cchTextMax = ple->title.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // artist
        lvi.pszText = (char *)ple->artist.Get();
        lvi.cchTextMax = ple->artist.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // album
        lvi.pszText = (char *)ple->album.Get();
        lvi.cchTextMax = ple->album.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // length
        int minutes = 0;
        int seconds = 0;
        double time = 0.0;
        time = ple->length;
        if (WDL_DefinitelyGreaterThan(time, 0.0))
        {
          minutes = (int)time / 60;
          seconds = (int)time % 60;
        }
        m_strbuf.SetFormatted(128, "%01d:%02d", minutes, seconds);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // genre
        lvi.pszText = (char *)ple->genre.Get();
        lvi.cchTextMax = ple->genre.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // samplerate
        m_strbuf.SetFormatted(128,
          "%.0f Hz", ple->srate);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // filename
        lvi.pszText = (char *)ple->filename.Get();
        lvi.cchTextMax = ple->filename.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // filepath
        lvi.pszText = (char *)ple->filepath.Get();
        lvi.cchTextMax = ple->filepath.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // fileext
        lvi.pszText = (char *)ple->fileext.Get();
        lvi.cchTextMax = ple->fileext.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // filesize
        double mb = (double)ple->filesize / 1024 / 1024;
        m_strbuf.SetFormatted(128, "%.2f MB", mb);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // comment
        lvi.pszText = (char *)ple->comment.Get();
        lvi.cchTextMax = ple->comment.GetLength();
        ListView_SetItem(m_list, &lvi);

        ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
        ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
        if (i == m_selstart) ListView_SetItemState(m_list, i, LVIS_FOCUSED, LVIS_FOCUSED);
        if (i >= m_selstart && i < m_selend)
        {
          WDL_ASSERT(m_selstart >= 0);
          WDL_ASSERT(m_selstart < m_selend);
          WDL_ASSERT(i < m_selend);
          ListView_SetItemState(m_list, i, LVIS_SELECTED, LVIS_SELECTED);
          if (i == m_selend - 1)
          {
            ListView_EnsureVisible(m_list, i, FALSE);
            m_selstart = m_selend = -1;
          }
        }
      }

      if (WDL_unlikely(m_marquee >= 0))
      {
        for (int i = 0; i < ListView_GetItemCount(m_list); i++)
        {
          ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
          ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
        }
        ListView_SetItemState(m_list, m_marquee, LVIS_FOCUSED, LVIS_FOCUSED);
        ListView_SetItemState(m_list, m_marquee, LVIS_SELECTED, LVIS_SELECTED);
        ListView_EnsureVisible(m_list, m_marquee, FALSE); m_marquee = -1;
      }
    }
    g_playlist->DoneWithPlayList();

    if (g_playlist->WantRefresh())
    {
      WDL_PtrList<THA_PlayList::PlayListEntry> *entries = g_playlist->GetPlayList();

      for (int i = g_playlist->GetRefreshStart();
        i < g_playlist->GetRefreshEnd(); i++)
      {
        THA_PlayList::PlayListEntry *ple = entries->Get(i);

        LVITEM lvi = { 0, };
        int si = 0;
        lvi.mask = LVIF_TEXT | LVIF_PARAM;
        lvi.lParam = (LPARAM)i;
        lvi.iItem = i;
        lvi.iSubItem = si++; // type
        lvi.pszText = (char *)ple->type.Get();
        lvi.cchTextMax = ple->type.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // index
        m_strbuf.SetFormatted(128, "%d", i + 1);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // title
        lvi.pszText = (char *)ple->title.Get();
        lvi.cchTextMax = ple->title.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // artist
        lvi.pszText = (char *)ple->artist.Get();
        lvi.cchTextMax = ple->artist.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // album
        lvi.pszText = (char *)ple->album.Get();
        lvi.cchTextMax = ple->album.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // length
        int minutes = 0;
        int seconds = 0;
        double time = 0.0;
        time = ple->length;
        if (WDL_DefinitelyGreaterThan(time, 0.0))
        {
          minutes = (int)time / 60;
          seconds = (int)time % 60;
        }
        m_strbuf.SetFormatted(128, "%01d:%02d", minutes, seconds);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // genre
        lvi.pszText = (char *)ple->genre.Get();
        lvi.cchTextMax = ple->genre.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // samplerate
        m_strbuf.SetFormatted(128,
          "%.0f Hz", ple->srate);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // filename
        lvi.pszText = (char *)ple->filename.Get();
        lvi.cchTextMax = ple->filename.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // filepath
        lvi.pszText = (char *)ple->filepath.Get();
        lvi.cchTextMax = ple->filepath.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // fileext
        lvi.pszText = (char *)ple->fileext.Get();
        lvi.cchTextMax = ple->fileext.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // filesize
        double mb = ((double)ple->filesize / 1024.0 / 1024.0);
        m_strbuf.SetFormatted(128, "%.2f MB", mb);
        lvi.pszText = (char *)m_strbuf.Get();
        lvi.cchTextMax = m_strbuf.GetLength();
        ListView_SetItem(m_list, &lvi);

        lvi.mask = LVIF_TEXT;
        lvi.iSubItem = si++; // comment
        lvi.pszText = (char *)ple->comment.Get();
        lvi.cchTextMax = ple->comment.GetLength();
        ListView_SetItem(m_list, &lvi);

        ListView_SetItemState(m_list, i, ~LVIS_FOCUSED, LVIS_FOCUSED);
        ListView_SetItemState(m_list, i, ~LVIS_SELECTED, LVIS_SELECTED);
        if (i == m_selstart) ListView_SetItemState(m_list, i, LVIS_FOCUSED, LVIS_FOCUSED);
        if (i >= m_selstart && i < m_selend)
        {
          ListView_SetItemState(m_list, i, LVIS_SELECTED, LVIS_SELECTED);
          if (i == m_selend - 1) m_selstart = m_selend = -1;
        }
      }
      for (int i = 0; i < m_sel.GetSize(); i++)
      {
        if (!i) ListView_SetItemState(m_list, m_sel.Get()[i], LVIS_FOCUSED, LVIS_FOCUSED);
        ListView_SetItemState(m_list, m_sel.Get()[i], LVIS_SELECTED, LVIS_SELECTED);
      }
      m_sel.Resize(0);
    }
    g_playlist->DoneWithRefresh();

#if defined(THA_PL_INTEGRITY) && defined(_DEBUG)
    {
      int subitem = 0;
      char text[2048];
      ColumnHeader *ch = m_column_header.Get();

      for (int i = 0; i < m_column_header.GetSize(); i++)
      {
        if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
        {
          subitem = i;
          break;
        }
      }
      g_playlist->LockEnter();
      WDL_PtrList<THA_PlayList::PlayListEntry> *entries = g_playlist->GetPlayList();
      WDL_ASSERT(entries->GetSize() == ListView_GetItemCount(m_list));

      for (int i = 0; i < entries->GetSize(); i++)
      {
        ListView_GetItemText(m_list, i, subitem, text, sizeof(text));
        THA_PlayList::PlayListEntry *ple = entries->Get(i);
        WDL_ASSERT(!wdl_filename_cmp(ple->filepath.Get(), text));
      }
      g_playlist->LockLeave();
    }
#endif
  }
}

void THA_MainWnd::OnPaint(WPARAM wparam, LPARAM lparam)
{
  RECT r;
  GetClientRect(m_hwnd, &r);
  m_vwnd.SetPosition(&r);
  int w = r.right - r.left;
  int h = r.bottom - r.top;

  m_painter.PaintBegin(m_hwnd, RGB(255, 255, 255));
  int xo, yo;
  LICE_IBitmap *bm = m_painter.GetBuffer(&xo, &yo);

  WDL_VWnd *time = m_vwnd.GetChildByID(0);
  time->GetPosition(&r);

  if (m_scandb)
  {
    LICE_FillRect(bm, r.left, 4, 4, 4, LICE_RGBA(240, 55, 23, 255));
  }
  else
  {
    LICE_FillRect(bm, r.left, 4, 4, 4, LICE_RGBA(255, 255, 255, 255));
  }

  RECT tr;
  GetWindowRect(m_tree, &tr);
  ScreenToClient(m_hwnd, (LPPOINT)&tr);
  ScreenToClient(m_hwnd, ((LPPOINT)&tr)+1);

  if (m_splitter)
  {
    LICE_FillRect(bm, tr.right + 1, tr.top, 3, tr.bottom - tr.top, LICE_RGBA(55, 141, 247, 255));
  }
  else
  {
    LICE_FillRect(bm, tr.right + 1, tr.top, 3, tr.bottom - tr.top, LICE_RGBA(255, 255, 255, 255));
  }

  GetWindowRect(m_edit, &r);
  ScreenToClient(m_hwnd, (LPPOINT)&r);
  ScreenToClient(m_hwnd, ((LPPOINT)&r)+1);

  m_painter.PaintVirtWnd(&m_vwnd);
  m_painter.PaintEnd();

  //HDC hdc;
  //PAINTSTRUCT ps;
  //hdc = GetDC(m_list);//BeginPaint(m_list, &ps);
  //HPEN pen = CreatePen(PS_SOLID, 2, RGB(0, 0, 255));
  //SelectObject(hdc, pen);
  //MoveToEx(hdc, 0, 100, NULL);
  //LineTo(hdc, 100, 100);
  //DeleteObject(pen);
  //EndPaint(m_list, &ps);
}

INT_PTR THA_MainWnd::OnNotify(WPARAM wparam, LPARAM lparam)
{
  switch (LOWORD(wparam))
  {
  case IDC_TREE1:
    {
      switch (((LPNMHDR)lparam)->code)
      {
      case TVN_SELCHANGED:
        {

        } break;
      case NM_RCLICK:
        {
          HTREEITEM nit = TreeView_GetNextItem(((LPNMHDR)lparam)->hwndFrom, 0, TVGN_DROPHILITE);
          if (nit) TreeView_SelectItem(((LPNMHDR)lparam)->hwndFrom, nit);

          HTREEITEM it = TreeView_GetSelection(m_tree);
          if (it)
          {
            POINT pt;
            GetCursorPos(&pt);
            TrackPopupMenu(m_database_menu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hwnd, NULL);
          }
        } break;
      }
    }
    break;
  case IDC_LIST1:
    {
      switch (((LPNMHDR)lparam)->code)
      {
      case LVN_COLUMNCLICK:
        {
          LPNMLISTVIEW lv = (LPNMLISTVIEW)lparam;
          if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) &&
            !(GetAsyncKeyState(VK_SHIFT) & 0x8000) &&
            !(GetAsyncKeyState(VK_MENU) & 0x8000))
          {
            Sort(lv->iSubItem, true, false);
          }
          else
          {
            Sort(lv->iSubItem, false, false);
          }
        } break;
      case NM_RCLICK:
        {
          int nit = 0;
          int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
          while (pos != -1)
          {
            nit++;
            if (nit > 1) break;
            pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
          }
          pos = ListView_GetNextItem(m_list, -1, LVNI_FOCUSED|LVNI_SELECTED);
          if (pos >= 0)
          {
            POINT pt;
            GetCursorPos(&pt);

            if (g_playlist->GetPasteSize())
            {
              EnableMenuItem(m_playlist_menu, ID_PLAYLIST_PASTE, MF_ENABLED);
            }
            else
            {
              EnableMenuItem(m_playlist_menu, ID_PLAYLIST_PASTE, MF_GRAYED);
            }

            if (nit > 1)
            {
              EnableMenuItem(m_playlist_menu, 3, MF_BYPOSITION|MF_ENABLED);
            }
            else
            {
              EnableMenuItem(m_playlist_menu, 3, MF_BYPOSITION|MF_GRAYED);
            }

            if (nit == 1)
            {
              EnableMenuItem(m_playlist_menu, 12, MF_BYPOSITION|MF_ENABLED);
              EnableMenuItem(m_playlist_menu, 14, MF_BYPOSITION|MF_ENABLED);
              EnableMenuItem(m_playlist_menu, 15, MF_BYPOSITION|MF_ENABLED);
              EnableMenuItem(m_playlist_menu, 16, MF_BYPOSITION|MF_ENABLED);
              EnableMenuItem(m_playlist_menu, 17, MF_BYPOSITION|MF_ENABLED);

              int subitem = 0;
              char text[2048];
              ColumnHeader *ch = m_column_header.Get();

              for (int i = 0; i < m_column_header.GetSize(); i++)
              {
                if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
                {
                  subitem = i;
                  break;
                }
              }

              ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

              WDL_String url(text);
              url.DeleteSub(4, url.GetLength() - 4);
              for (int i = 0; i < url.GetLength(); i++)
              {
                url.Get()[i] = tolower_safe(url.Get()[i]);
              }

              if (!strncmp(url.Get(), "http", 4))
              {
                EnableMenuItem(m_playlist_menu, 14, MF_BYPOSITION|MF_GRAYED);
                EnableMenuItem(m_playlist_menu, 15, MF_BYPOSITION|MF_GRAYED);
                EnableMenuItem(m_playlist_menu, 16, MF_BYPOSITION|MF_GRAYED);
                EnableMenuItem(m_playlist_menu, 17, MF_BYPOSITION|MF_GRAYED);
              }
            }
            else
            {
              EnableMenuItem(m_playlist_menu, 12, MF_BYPOSITION|MF_GRAYED);
              EnableMenuItem(m_playlist_menu, 14, MF_BYPOSITION|MF_ENABLED);
              EnableMenuItem(m_playlist_menu, 15, MF_BYPOSITION|MF_ENABLED);
              EnableMenuItem(m_playlist_menu, 16, MF_BYPOSITION|MF_GRAYED);
              EnableMenuItem(m_playlist_menu, 17, MF_BYPOSITION|MF_GRAYED);
            }

            TrackPopupMenu(m_playlist_menu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hwnd, NULL);
          }
        } break;
      case LVN_BEGINDRAG:
        {
          m_lv_drag = true;
          SetFocus(m_list);
          SetCapture(m_hwnd);
        } break;
      case NM_DBLCLK:
        {
          StartPlayback();
        } break;
      case NM_CUSTOMDRAW:
        {
          LPNMLVCUSTOMDRAW  lplvcd = (LPNMLVCUSTOMDRAW)lparam;
          if (m_lv_drag && ListView_GetItemState(m_list, lplvcd->nmcd.dwItemSpec, LVIS_FOCUSED) == (LVIS_FOCUSED))
          {
            HPEN pen = CreatePen(PS_SOLID, 2, RGB(0, 0, 0));

            RECT r;
            ListView_GetItemRect(m_list, lplvcd->nmcd.dwItemSpec, &r, LVIR_BOUNDS);

            SelectObject(lplvcd->nmcd.hdc, pen);
            MoveToEx(lplvcd->nmcd.hdc, r.left, r.bottom - 2, NULL);
            LineTo(lplvcd->nmcd.hdc, r.right, r.bottom - 2);
            MoveToEx(lplvcd->nmcd.hdc, r.left, r.top + 2, NULL);
            LineTo(lplvcd->nmcd.hdc, r.right, r.top + 2);
            DeleteObject(pen);

            SetWindowLongPtr(m_hwnd, DWLP_MSGRESULT, CDRF_NOTIFYSUBITEMDRAW);
            return TRUE;
          }

          int result = CDRF_DODEFAULT;
          switch (lplvcd->nmcd.dwDrawStage)
          {
            case CDDS_PREPAINT:
              {
                result = CDRF_NOTIFYITEMDRAW;
              }
              break;
            case CDDS_ITEMPREPAINT:
              result = CDRF_NOTIFYSUBITEMDRAW;
              break;
            case CDDS_SUBITEM | CDDS_ITEMPREPAINT: // Before a subitem is drawn.
            {
              switch (lplvcd->iSubItem)
              {
                case 0: // 1st column
                {
                  if (lplvcd->nmcd.dwItemSpec == m_activesel)
                  {
                    int at = g_preferences->GetActiveTextColor();
                    int bk = g_preferences->GetBackgroundColor();
                    lplvcd->clrText = RGB((at >> 16) & 0xFF,(at >> 8) & 0xFF, at & 0xFF);
                    lplvcd->clrTextBk = RGB((bk >> 16) & 0xFF,(bk >> 8) & 0xFF, bk & 0xFF);
                  }
                  else
                  {
                    int bk = g_preferences->GetBackgroundColor();
                    int fg = g_preferences->GetTextColor();
                    lplvcd->clrTextBk = RGB((bk >> 16) & 0xFF,(bk >> 8) & 0xFF, bk & 0xFF);
                    lplvcd->clrText = RGB((fg >> 16) & 0xFF,(fg >> 8) & 0xFF, fg & 0xFF);
                  }

                  return CDRF_NEWFONT;
                } break;
              }
            }
          }

          SetWindowLongPtr(m_hwnd, DWLP_MSGRESULT, result);
          return TRUE;
        } break;
      }
    } break;
  }

  return 0;
}

void THA_MainWnd::OnContextMenu(WPARAM wparam, LPARAM lparam)
{
  POINT pt;
  pt.x = GET_X_LPARAM(lparam);
  pt.y = GET_Y_LPARAM(lparam);
  HANDLE hwnd = (HANDLE)wparam;

  if (hwnd == m_list && pt.x == -1 && pt.y == -1)
  {
    int nit = 0;
    int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
    while (pos != -1)
    {
      nit++;
      if (nit > 1) break;
      pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
    }
    pos = ListView_GetNextItem(m_list, -1, LVNI_FOCUSED|LVNI_SELECTED);
    if (pos >= 0)
    {
      RECT r;
      ListView_EnsureVisible(m_list, pos, FALSE);
      ListView_GetItemRect(m_list, pos, &r, LVIR_BOUNDS);
      pt.x = r.left;
      pt.y = r.bottom;

      GetClientRect(m_list, &r);
      if (pt.x < r.left)
      {
        pt.x = r.left;
      }

      if (g_playlist->GetPasteSize())
      {
        EnableMenuItem(m_playlist_menu, ID_PLAYLIST_PASTE, MF_ENABLED);
      }
      else
      {
        EnableMenuItem(m_playlist_menu, ID_PLAYLIST_PASTE, MF_GRAYED);
      }

      if (nit > 1)
      {
        EnableMenuItem(m_playlist_menu, 3, MF_BYPOSITION|MF_ENABLED);
      }
      else
      {
        EnableMenuItem(m_playlist_menu, 3, MF_BYPOSITION|MF_GRAYED);
      }

      if (nit == 1)
      {
        EnableMenuItem(m_playlist_menu, 12, MF_BYPOSITION|MF_ENABLED);
        EnableMenuItem(m_playlist_menu, 14, MF_BYPOSITION|MF_ENABLED);
        EnableMenuItem(m_playlist_menu, 15, MF_BYPOSITION|MF_ENABLED);
        EnableMenuItem(m_playlist_menu, 16, MF_BYPOSITION|MF_ENABLED);
        EnableMenuItem(m_playlist_menu, 17, MF_BYPOSITION|MF_ENABLED);

        int subitem = 0;
        char text[2048];
        ColumnHeader *ch = m_column_header.Get();

        for (int i = 0; i < m_column_header.GetSize(); i++)
        {
          if (ch[i].id == ID_LIBRARY_HEADER_FILEPATH)
          {
            subitem = i;
            break;
          }
        }

        ListView_GetItemText(m_list, pos, subitem, text, sizeof(text));

        WDL_String url(text);
        url.DeleteSub(4, url.GetLength() - 4);
        for (int i = 0; i < url.GetLength(); i++)
        {
          url.Get()[i] = tolower_safe(url.Get()[i]);
        }

        if (!strncmp(url.Get(), "http", 4))
        {
          EnableMenuItem(m_playlist_menu, 14, MF_BYPOSITION|MF_GRAYED);
          EnableMenuItem(m_playlist_menu, 15, MF_BYPOSITION|MF_GRAYED);
          EnableMenuItem(m_playlist_menu, 16, MF_BYPOSITION|MF_GRAYED);
          EnableMenuItem(m_playlist_menu, 17, MF_BYPOSITION|MF_GRAYED);
        }
      }
      else
      {
        EnableMenuItem(m_playlist_menu, 12, MF_BYPOSITION|MF_GRAYED);
        EnableMenuItem(m_playlist_menu, 14, MF_BYPOSITION|MF_ENABLED);
        EnableMenuItem(m_playlist_menu, 15, MF_BYPOSITION|MF_ENABLED);
        EnableMenuItem(m_playlist_menu, 16, MF_BYPOSITION|MF_GRAYED);
        EnableMenuItem(m_playlist_menu, 17, MF_BYPOSITION|MF_GRAYED);
      }

      ClientToScreen(m_list, (LPPOINT)&pt);
      TrackPopupMenu(m_playlist_menu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hwnd, NULL);
    }
  }

  if (hwnd == m_list)
  {
    //POINT pt;
    //pt.x = GET_X_LPARAM(lparam);
    //pt.y = GET_Y_LPARAM(lparam);
    ScreenToClient(m_list, (LPPOINT)&pt);

    HWND h = ListView_GetHeader(GetDlgItem(m_hwnd, IDC_LIST1));
    RECT r;
    GetWindowRect(h, &r);
    ScreenToClient(m_list, (LPPOINT)&r);
    ScreenToClient(m_list, ((LPPOINT)&r)+1);

    ColumnHeader *ch = m_column_header.Get();

    EnableMenuItem(m_column_menu, ID_LIBRARY_HEADER_INFO, MF_GRAYED);

#if defined(__APPLE__) || defined(__linux__)
    r.bottom = r.top + SWELL_GetListViewHeaderHeight(m_list);
#endif

    if (pt.x >= r.left && pt.y >= r.top && pt.x <= r.right && pt.y <= r.bottom)
    {
      for (int i = 0; i < m_column_header.GetSize(); i++)
      {
        if (ListView_GetColumnWidth(m_list, i))
        {
          CheckMenuItem(m_column_menu, ch[i].id, MF_CHECKED);
        }
        else
        {
          CheckMenuItem(m_column_menu, ch[i].id, MF_UNCHECKED);
        }
      }

      ClientToScreen(m_list, (LPPOINT)&pt);
      TrackPopupMenu(m_column_menu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hwnd, NULL);
    }
  }

  if (hwnd == m_tree && pt.x == -1 && pt.y == -1)
  {
    HTREEITEM it = TreeView_GetSelection(m_tree);

    if (it)
    {
      RECT r;
      TreeView_EnsureVisible(m_tree, it);
      TreeView_GetItemRect(m_tree, it, &r, TRUE);

      POINT pt;
      pt.x = r.left;
      pt.y = r.bottom;

      GetClientRect(m_tree, &r);
      if (pt.x < r.left)
      {
        TreeView_GetItemRect(m_tree, it, &r, TRUE);
        pt.x = r.left;
        pt.y = r.bottom;
      }

      ClientToScreen(m_tree, &pt);
      TrackPopupMenu(m_database_menu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hwnd, NULL);
    }
  }

  if (hwnd == m_hwnd)
  {
    ScreenToClient(m_hwnd, (LPPOINT)&pt);

    RECT timerec;
    m_time->GetPosition(&timerec);
    if (pt.x >= timerec.left && pt.y >= timerec.top &&
      pt.x <= timerec.right && pt.y <= timerec.bottom)
    {
      if (!m_timestate)
      {
        CheckMenuItem(m_time_menu, ID_TIME_TIMEELAPSED, MF_CHECKED);
        CheckMenuItem(m_time_menu, ID_TIME_TIMEREMAINING, MF_UNCHECKED);
      }
      else
      {
        CheckMenuItem(m_time_menu, ID_TIME_TIMEELAPSED, MF_UNCHECKED);
        CheckMenuItem(m_time_menu, ID_TIME_TIMEREMAINING, MF_CHECKED);
      }

      if (g_rendersystem->IsActive())
      {
        EnableMenuItem(m_time_menu, ID_TIME_STOP, MF_ENABLED);
      }
      else
      {
        EnableMenuItem(m_time_menu, ID_TIME_STOP, MF_GRAYED);
      }

      ClientToScreen(m_hwnd, (LPPOINT)&pt);
      TrackPopupMenu(m_time_menu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hwnd, NULL);
    }
  }
}

void THA_MainWnd::OnLButtonDown(WPARAM wparam, LPARAM lparam)
{
  POINT pt;
  pt.x = GET_X_LPARAM(lparam);
  pt.y = GET_Y_LPARAM(lparam);

  RECT tr, lr;
  GetWindowRect(m_tree, &tr);
  ScreenToClient(m_hwnd, (LPPOINT)&tr);
  ScreenToClient(m_hwnd, ((LPPOINT)&tr)+1);
  GetWindowRect(m_list, &lr);
  ScreenToClient(m_hwnd, (LPPOINT)&lr);
  ScreenToClient(m_hwnd, ((LPPOINT)&lr)+1);

  int space = lr.left - tr.right;

  if (pt.x >= tr.right && pt.x <= tr.right + space &&
    pt.y >= tr.top && pt.y <= tr.bottom)
  {
    m_splitter = true;
    SetCursor(LoadCursor(NULL, IDC_SIZEWE));
    InvalidateRect(m_hwnd, NULL, FALSE);
    SetCapture(m_hwnd);
  }
}

void THA_MainWnd::OnLButtonUp(WPARAM wparam, LPARAM lparam)
{
  if (m_lv_drag)
  {
    m_lv_drag = false;

    WDL_TypedBuf<int> selected;

    int pos = ListView_GetNextItem(m_list, -1, LVNI_SELECTED);
    while (pos != -1)
    {
      selected.Add(pos);
      pos = ListView_GetNextItem(m_list, pos, LVNI_SELECTED);
    }

    m_selstart = m_dragitem;
    if (selected.GetSize())
    {
      if (m_selstart > selected.Get()[0])
      {
        m_selstart -= selected.GetSize() - 1;
      }
    }
    m_selend = m_selstart + selected.GetSize();

    g_playlist->MoveSelection(&selected, m_dragitem);

    m_dragitem = -1;
    ReleaseCapture();
  }

  if (m_splitter)
  {
    m_splitter = false;
    InvalidateRect(m_hwnd, NULL, FALSE);
    ReleaseCapture();
  }
}

void THA_MainWnd::OnMouseMove(WPARAM wparam, LPARAM lparam)
{
  if (m_lv_drag)
  {
    LVHITTESTINFO ht;
    ht.pt.x = LOWORD(lparam);
    ht.pt.y = HIWORD(lparam);
    ClientToScreen(m_hwnd, &ht.pt);
    ScreenToClient(m_list, &ht.pt);
    ListView_HitTest(m_list, &ht);

    if (ht.iItem >= 0)
    {
      m_dragitem = ht.iItem;
    }

    if (ht.iItem == -1)
    {
      ListView_SetItemState(m_list, -1, ~LVIS_FOCUSED, LVIS_FOCUSED);
    }
    else
    {
      ListView_SetItemState(m_list, -1, ~LVIS_FOCUSED, LVIS_FOCUSED);
      ListView_SetItemState(m_list, ht.iItem, LVIS_FOCUSED, LVIS_FOCUSED);
    }
  }

  POINT pt;
  pt.x = GET_X_LPARAM(lparam);
  pt.y = GET_Y_LPARAM(lparam);

  RECT tr, lr;
  GetWindowRect(m_tree, &tr);
  ScreenToClient(m_hwnd, (LPPOINT)&tr);
  ScreenToClient(m_hwnd, ((LPPOINT)&tr)+1);
  GetWindowRect(m_list, &lr);
  ScreenToClient(m_hwnd, (LPPOINT)&lr);
  ScreenToClient(m_hwnd, ((LPPOINT)&lr)+1);

  int space = lr.left - tr.right;

  if (pt.x >= tr.right && pt.x <= tr.right + space &&
    pt.y >= tr.top && pt.y <= tr.bottom)
  {
    SetCursor(LoadCursor(NULL, IDC_SIZEWE));
  }

  if (m_splitter)
  {
    if (pt.x >= tr.left + 50 && pt.x <= lr.right - 50 - space)
    {
      WDL_WndSizer__rec *rectree = m_resize.get_itembywnd(m_tree);
      WDL_WndSizer__rec *reclist = m_resize.get_itembywnd(m_list);
      WDL_WndSizer__rec *recedit = m_resize.get_itembywnd(m_edit);
      WDL_WndSizer__rec *rectime = m_resize.get_itembyvirt(m_vwnd.GetChildByID(0));
      WDL_WndSizer__rec *recinfo = m_resize.get_itembyvirt(m_vwnd.GetChildByID(1));
      if (rectree && reclist && recedit && rectime && recinfo)
      {
        rectree->orig.right = pt.x;
        reclist->orig.left = pt.x + space;
        recedit->orig.right = pt.x;
        rectime->orig.right = pt.x / 2;
        recinfo->orig.left = pt.x / 2;
        recinfo->orig.right = pt.x;

        int treew = rectree->last.right - rectree->last.left;
        int listw = reclist->last.right - reclist->last.left;
        m_strbuf.SetFormatted(512, "%s | DB: %d, PL: %d",
          m_titleorig.Get(), treew, listw);
        SetTitle(m_strbuf.Get());
      }

      m_resize.onResize();
    }
  }
}

INT_PTR THA_MainWnd::OnCtrlColorEdit(UINT msg, WPARAM wparam, LPARAM lparam)
{
  if((HWND)lparam == GetDlgItem(m_hwnd, IDC_EDIT1))
  {
    int bk = g_preferences->GetBackgroundColor();
    int fg = g_preferences->GetTextColor();
    SetBkMode((HDC)wparam, RGB((bk >> 16) & 0xFF,(bk >> 8) & 0xFF, bk & 0xFF));
    SetTextColor((HDC)wparam, RGB((fg >> 16) & 0xFF,(fg >> 8) & 0xFF, fg & 0xFF));
    if (!m_brush) m_brush = CreateSolidBrush(RGB((bk >> 16) & 0xFF,(bk >> 8) & 0xFF, bk & 0xFF));
    return (INT_PTR)m_brush;
  }
  else
  {
    return DefWindowProc(m_hwnd, msg, wparam, lparam);
  }
}

void THA_MainWnd::OnActivateApp(WPARAM wparam, LPARAM lparam)
{
  if (g_rendersystem && !g_rendersystem->IsActive())
  {
    if (wparam == TRUE)
    {
      if (!g_audiostreamer->IsRunning())
      {
        double wall0 = time_precise();
        if (!g_audiostreamer->Open())
        {
          int fmt = g_preferences->GetAudioDeviceBitDepth();
          int srate = (int)g_preferences->GetAudioDeviceSamplerate();
          int nch = g_preferences->GetAudioDeviceOutputChannels();

          WDL_FastString fmtstr;

          switch (fmt)
          {
          case 0: fmtstr.Set("16-bit"); break;
          case 1: fmtstr.Set("24-bit"); break;
          case 2: fmtstr.Set("32-bit"); break;
          case 3: fmtstr.Set("32-bit FP"); break;
          default: fmtstr.Set("Unknown"); break;
          }

          WDL_FastString err;
          err.SetFormatted(1024,
            THA_NAME_MARKETING " cannot open the audio device with the following settings:\n\n"
            "Audio bit depth: %s\n"
            "Audio sample rate: %d\n"
            "Audio output channels: %d\n\n"
            "Make sure that the related settings are correct in " THA_NAME_MARKETING " preferences.",
            fmtstr.Get(), srate, nch);

          MessageBox(m_hwnd, err.Get(), "Audio device error", MB_OK);
        }
        else
        {
          WDL_ASSERT(!g_audiostreamer->IsRunning());
          if (!g_rendersystem->IsRunning())
          {
            g_rendersystem->StartThread();
          }
          double wall1 = time_precise();
          m_strbuf.SetFormatted(512, "%s | Online in %f sec",
            m_titleorig.Get(), wall1 - wall0);
          SetTitle(m_strbuf.Get());
        }
      }
    }
    else
    {
      if (!g_audiostreamer->IsRunning())
      {
        double wall0 = time_precise();
        g_audiostreamer->Close();
        g_rendersystem->StopThread();
        double wall1 = time_precise();
        m_strbuf.SetFormatted(512, "%s | Offline in %f sec",
          m_titleorig.Get(), wall1 - wall0);
        SetTitle(m_strbuf.Get());
      }
    }
  }
}

void THA_MainWnd::OnNCLButtonDown(WPARAM wparam, LPARAM lparam)
{
  m_titlecapture = true;
  SetTimer(m_hwnd, THA_DISABLE_CONTROLS, THA_DISABLE_CONTROLS_MS, NULL);
  SendMessage(m_list, WM_SETREDRAW, FALSE, 0);
}

WDL_DLGRET THA_MainWnd::NewTreeProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
  switch (msg)
  {
  case WM_MOUSEWHEEL:
  case WM_MOUSEHWHEEL:
    if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
    {
      return 0;
    }
    break;
  case WM_SYSKEYUP:
  case WM_KEYUP:
    {
      switch(wparam)
      {
      case VK_UP:
        {
          HTREEITEM sel = TreeView_GetSelection(s_tree);
          if (!sel) return 0;

          if (sel == TreeView_GetRoot(s_tree) && s_istreeroot)
          {
            s_istreeroot = false;
            SetFocus(s_edit);
          }
          else if (sel == TreeView_GetRoot(s_tree))
          {
            s_istreeroot = true;
          }
          else
          {
            s_istreeroot = false;
          }
        }
        break;
      }
    }
    break;
  }

  return CallWindowProc(PrevTreeProc, hwnd, msg, wparam, lparam);
}

WDL_DLGRET THA_MainWnd::NewListProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
  switch (msg)
  {
  case WM_MOUSEWHEEL:
  case WM_MOUSEHWHEEL:
    if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
    {
      return 0;
    }
    break;
  case WM_SETFOCUS:
    {
      int one_ping_only = ListView_GetNextItem(s_list, -1, LVNI_SELECTED);
      if (one_ping_only == -1)
      {
        ListView_SetItemState(s_list, 0,
          LVNI_SELECTED|LVNI_FOCUSED,
          LVNI_SELECTED|LVNI_FOCUSED)
      }
    }
    break;
  }

  return CallWindowProc(PrevListProc, hwnd, msg, wparam, lparam);
}

WDL_DLGRET THA_MainWnd::NewEditProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
  switch (msg)
  {
  case WM_MOUSEWHEEL:
  case WM_MOUSEHWHEEL:
    if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
    {
      return 0;
    }
    break;
  case WM_SYSKEYDOWN:
  case WM_KEYDOWN:
    {
      switch(wparam)
      {
      case VK_HOME:
        {
          if (g_mainwnd->IsPlayListFind())
          {
            if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) &&
              !(GetAsyncKeyState(VK_SHIFT) & 0x8000) &&
              !(GetAsyncKeyState(VK_MENU) & 0x8000))
            {
              ListView_EnsureVisible(s_list, 0, FALSE);
              return 0;
            }
          }
        } break;
      case VK_END:
        {
          if (g_mainwnd->IsPlayListFind())
          {
            if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) &&
              !(GetAsyncKeyState(VK_SHIFT) & 0x8000) &&
              !(GetAsyncKeyState(VK_MENU) & 0x8000))
            {
              int bot = ListView_GetItemCount(s_list) - 1;
              ListView_EnsureVisible(s_list, bot, FALSE);
              return 0;
            }
          }
        } break;
      case VK_PRIOR:
        {
          if (g_mainwnd->IsPlayListFind())
          {
            ListView_Scroll(s_list, 0, 6 * -16);
          }
        } break;
      case VK_NEXT:
        {
          if (g_mainwnd->IsPlayListFind())
          {
            ListView_Scroll(s_list, 0, 6 * 16);
          }
        } break;
      case VK_UP:
        {
          if (g_mainwnd->IsPlayListFind())
          {
            ListView_Scroll(s_list, 0, -16);
            return 0;
          }
        } break;
      case VK_DOWN:
        {
          if (g_mainwnd->IsPlayListFind())
          {
            ListView_Scroll(s_list, 0, 16);
            return 0;
          }
          else
          {
            HWND tree = GetDlgItem(g_mainwnd->Handle(), IDC_TREE1);
            TreeView_Select(s_edit, TVI_ROOT, TVGN_CARET);
            SetFocus(tree);
          }
        }
        break;
      case VK_BACK:
        {
          if ((GetAsyncKeyState(VK_CONTROL) & 0x8000))
          {
            SetTimer(g_mainwnd->Handle(), THA_CLEAR_EDIT,
              THA_CLEAR_EDIT_MS, NULL);
          }
        }
        break;
      }
    }
    break;
  case WM_SETFOCUS:
    {
      g_mainwnd->ShowEditLabel();
    } break;
  case WM_KILLFOCUS:
    {
      g_mainwnd->HideEditLabel();
    } break;
  }

  return CallWindowProc(PrevEditProc, hwnd, msg, wparam, lparam);
}

WDL_DLGRET THA_MainWnd::MainWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
  THA_MainWnd *self = (THA_MainWnd *)GetWindowLongPtr(hwnd, GWLP_USERDATA);

  if (!self && msg == WM_INITDIALOG)
  {
    SetWindowLongPtr(hwnd, GWLP_USERDATA, lparam);
    self = (THA_MainWnd *)lparam;
    self->m_hwnd = hwnd;
  }

  if (WDL_likely(self))
  {
    return self->MainWndLoop(msg, wparam, lparam);
  }
  else
  {
    return 0;
  }
}
WDL_DLGRET THA_AddURLWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
  switch(msg)
  {
    case WM_INITDIALOG:
    {
      g_urlwnd = hwnd;
      int x = GetPrivateProfileInt(THA_NAME, "url_wnd_x", 0, g_inipath.Get());
      int y = GetPrivateProfileInt(THA_NAME, "url_wnd_y", 50, g_inipath.Get());
      SetWindowPos(hwnd, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);

      ShowWindow(hwnd, SW_SHOWDEFAULT);
    } break;
    case WM_DESTROY:
    {
      RECT r;
      GetWindowRect(hwnd, &r);

      int x = r.left;
      int y = r.top;

      WDL_FastString xstr, ystr;
      xstr.SetFormatted(32, "%d", x);
      ystr.SetFormatted(32, "%d", y);

      WritePrivateProfileString(THA_NAME, "url_wnd_x", xstr.Get(), g_inipath.Get());
      WritePrivateProfileString(THA_NAME, "url_wnd_y", ystr.Get(), g_inipath.Get());
      g_urlwnd = hwnd = NULL;
    } break;
    case WM_COMMAND:
    {
      switch(LOWORD(wparam))
      {
      case IDOK:
        {
          char text[1024];
          HWND edit = GetDlgItem(hwnd, IDC_EDIT1);
          GetWindowText(edit, text, sizeof(text));
          g_mainwnd->AddURL(text);
          DestroyWindow(hwnd);
        } break;
      case IDCANCEL:
        {
          DestroyWindow(hwnd);
        } break;
      }
    } break;
  }

  return 0;
}

WDL_DLGRET THA_GotoIndexWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
  switch(msg)
  {
    case WM_INITDIALOG:
    {
      g_indexwnd = hwnd;
      int x = GetPrivateProfileInt(THA_NAME, "index_wnd_x", 0, g_inipath.Get());
      int y = GetPrivateProfileInt(THA_NAME, "index_wnd_y", 50, g_inipath.Get());
      SetWindowPos(hwnd, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);

      ShowWindow(hwnd, SW_SHOWDEFAULT);
    } break;
    case WM_DESTROY:
    {
      RECT r;
      GetWindowRect(hwnd, &r);

      int x = r.left;
      int y = r.top;

      WDL_FastString xstr, ystr;
      xstr.SetFormatted(32, "%d", x);
      ystr.SetFormatted(32, "%d", y);

      WritePrivateProfileString(THA_NAME, "index_wnd_x", xstr.Get(), g_inipath.Get());
      WritePrivateProfileString(THA_NAME, "index_wnd_y", ystr.Get(), g_inipath.Get());
      g_indexwnd = hwnd = NULL;
    } break;
    case WM_COMMAND:
    {
      switch(LOWORD(wparam))
      {
      case IDOK:
        {
          int index = GetDlgItemInt(hwnd, IDC_EDIT1, NULL, TRUE);
          g_mainwnd->GotoIndex(index - 1);
          DestroyWindow(hwnd);
        } break;
      case IDCANCEL:
        {
          DestroyWindow(hwnd);
        } break;
      }
    } break;
  }

  return 0;
}

WDL_DLGRET THA_MoveToIndexWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
  switch(msg)
  {
    case WM_INITDIALOG:
    {
      g_movewnd = hwnd;
      int x = GetPrivateProfileInt(THA_NAME, "move_wnd_x", 0, g_inipath.Get());
      int y = GetPrivateProfileInt(THA_NAME, "move_wnd_y", 50, g_inipath.Get());
      SetWindowPos(hwnd, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);

      ShowWindow(hwnd, SW_SHOWDEFAULT);
    } break;
    case WM_DESTROY:
    {
      RECT r;
      GetWindowRect(hwnd, &r);

      int x = r.left;
      int y = r.top;

      WDL_FastString xstr, ystr;
      xstr.SetFormatted(32, "%d", x);
      ystr.SetFormatted(32, "%d", y);

      WritePrivateProfileString(THA_NAME, "move_wnd_x", xstr.Get(), g_inipath.Get());
      WritePrivateProfileString(THA_NAME, "move_wnd_y", ystr.Get(), g_inipath.Get());
      g_movewnd = hwnd = NULL;
    } break;
    case WM_COMMAND:
    {
      switch(LOWORD(wparam))
      {
      case IDOK:
        {
          int index = GetDlgItemInt(hwnd, IDC_EDIT1, NULL, TRUE);
          g_mainwnd->MoveToIndex(index - 1);
          DestroyWindow(hwnd);
        } break;
      case IDCANCEL:
        {
          DestroyWindow(hwnd);
        } break;
      }
    } break;
  }

  return 0;
}

WDL_DLGRET THA_MainWnd::MainWndLoop(UINT msg, WPARAM wparam, LPARAM lparam)
{
#if defined(__linux__)
  if (msg == WM_KEYDOWN && LOWORD(wparam) == VK_RETURN)
  {
    SendMessage(m_hwnd, WM_COMMAND, MAKEWPARAM(IDOK, 0), 0);
    return 0;
  }
#endif

  switch(msg)
  {
    case WM_INITDIALOG: OnInitDialog(wparam, lparam); break;
    case WM_SYSCOMMAND: OnSysCommand(wparam, lparam); break;
    case WM_CLOSE: return OnClose(wparam, lparam); break;
    case WM_SIZE: OnSize(wparam, lparam); break;
    case WM_MOVE: OnMove(wparam, lparam); break;
    case WM_DESTROY: OnDestroy(wparam, lparam); break;
    case WM_COMMAND: OnCommand(wparam, lparam); break;
    case WM_TIMER: OnTimer(wparam, lparam); break;
    case WM_PAINT: OnPaint(wparam, lparam); break;
    case WM_ERASEBKGND: return 1; break;
    case WM_NOTIFY: return OnNotify(wparam, lparam); break;
    case WM_CONTEXTMENU: OnContextMenu(wparam, lparam); break;
    case WM_LBUTTONDOWN: OnLButtonDown(wparam, lparam); break;
    case WM_LBUTTONUP: OnLButtonUp(wparam, lparam); break;
    case WM_MOUSEMOVE: OnMouseMove(wparam, lparam); break;
    //case WM_CTLCOLOREDIT: return OnCtrlColorEdit(msg, wparam, lparam); break;
    case WM_ACTIVATEAPP: OnActivateApp(wparam, lparam); break;
    case WM_NCLBUTTONDOWN: OnNCLButtonDown(wparam, lparam); break;
  }

  return 0;
}

#include "WDL/eel2/ns-eel.h"
#include "WDL/lice/lice.h"
#include "WDL/wingui/virtwnd-controls.h"
#include "WDL/wingui/scrollbar/coolscroll.h"

// TO BE IMPLEMENTED BY APP:
int WDL_STYLE_WantGlobalButtonBorders() { return 0; }
bool WDL_STYLE_WantGlobalButtonBackground(int *col) { return false; }
bool WDL_STYLE_GetBackgroundGradient(double *gradstart, double *gradslope) { return false; }
LICE_IBitmap *WDL_STYLE_GetSliderBitmap2(bool vert) { return NULL; }
int WDL_STYLE_AllowSliderMouseWheel(WDL_VWnd *vw, double *scale) { if (GetAsyncKeyState(VK_CONTROL)&0x8000) *scale /= 16.0; return 1; }
bool WDL_STYLE_AllowSliderClickOutsideHandle(WDL_VWnd *wv) { return true; }
int WDL_STYLE_GetSliderDynamicCenterPos() { return 500; }

// implemented by calling app, can return a LICE_IBitmap **img for "scrollbar"
void *GetIconThemePointer(const char *name)
{
  if (!strcmp(name, "scrollbar"))
  {}

  return NULL;
}

// can be a passthrough to GetSysColor()
int CoolSB_GetSysColor(HWND m_hwnd, int val)
{
  return GetSysColor(val);
}

#include <WDL/eel2/ns-eel.h>

void NSEEL_HOSTSTUB_EnterMutex()
{}

void NSEEL_HOSTSTUB_LeaveMutex()
{}
