#include "pch.h"

#include <ctime>
#include <process.h>
#include <string>
#include <cstdlib>
#include <windows.h>
#include <cwchar>      // for std::wcstoll
#include <cwctype>
#include <commctrl.h>
#include "xmpfunc.h"
#include "xmpdsp.h"
#include "resource.h"  // define IDD_SEARCH
#include "sqlite\sqlite3.h"
#include <Shlwapi.h>    // PathRemoveFileSpecW
#include <shellapi.h>   // for ShellExecuteW
#include <map>
#include <set>
#include <vector>
#include <urlmon.h>
#include "utf.hpp"
#include "time.hpp"
#include "http_head.hpp"
#include <memory>
#include "modland.hpp"
#include "pls.hpp"
#pragma comment(lib, "urlmon.lib")
#pragma comment(lib, "Shlwapi.lib")

static InterfaceProc g_faceproc;
static XMPFUNC_MISC* xmpfmisc;
static XMPFUNC_FILE* xmpffile;
// static XMPFUNC_REGISTRY* xmpfreg;

static void *WINAPI     Plugin_Init(void);
static void WINAPI     Plugin_Exit(void* inst);
static const char* WINAPI Plugin_GetDescription(void* inst);
static void WINAPI     MyConfig(void* inst, HWND win);
static DWORD WINAPI    Plugin_GetConfig(void* inst, void* config);
static BOOL WINAPI     Plugin_SetConfig(void* inst, void* config, DWORD size);
static BOOL CALLBACK   SearchDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
static void WINAPI     OpenSearchShortcut(void);
static bool EnsureDatabaseOpen(HWND hDlg);
static void DoSearch(HWND hDlg, bool exact = false, int randomCount = 0);

struct PluginConfig {
};

static HWND hSearchDlg = nullptr;

static HINSTANCE hInstance;
static HWND hWndConf = 0;

static HWND hWndXMP;

static sqlite3* g_db = nullptr;
// --- build info (caché en memoria) ---
static time_t        g_dbBuiltUnix = 0;          // epoch (UTC)
static std::wstring  g_dbBuiltIsoW;              // "YYYY-MM-DDTHH:MM:SSZ"
static bool          g_dbBuiltAllFormats = false; // true si se reconstruyó con 'allFormats'
static std::wstring DbBadge()
{
    if (!g_dbBuiltUnix) return L"";  // aún no sabemos la fecha

    std::tm tmLocal{};
#if defined(_WIN32)
    localtime_s(&tmLocal, &g_dbBuiltUnix);   // thread-safe en MSVC
#else
    localtime_r(&g_dbBuiltUnix, &tmLocal);
#endif

    wchar_t date[16] = L"";
    if (wcsftime(date, _countof(date), L"%Y-%m-%d", &tmLocal) == 0)
        return L"";

    std::wstring s = L"  -  db: ";
    s += date;                              // ej. 2025-03-21 (hora local)
    if (g_dbBuiltAllFormats) s += L" (all formats)";
    return s;
}

static int g_colInitWidth[5] = { 0, 0, 0, 0, 0 };

#define WM_DB_REBUILT    (WM_APP + 100)
#define WM_CHECK_READY   (WM_APP + 101)

struct CheckParams {
    HWND hDlg;
};

static unsigned __stdcall CheckUpdatesThread(void* pv)
{
    std::unique_ptr<CheckParams> p((CheckParams*)pv);
    time_t remoteUtc = 0;
    BOOL ok = HttpHead_LastModified(L"https://modland.com/allmods.zip", &remoteUtc) ? TRUE : FALSE;

    // Post result back.  Use LPARAM for the 64-bit time safely.
    PostMessageW(p->hDlg, WM_CHECK_READY, (WPARAM)ok, (LPARAM)remoteUtc);
    return 0;
}

#define IDT_SEARCH_DELAY      1
#define SEARCH_DELAY_MS     300   // 300 ms de debounce

// xmplay supported formats only (default)
static const std::set<std::string> xmplayFormats = {
    "mod","s3m","xm","it","mo3","mtm","umx"
};

// all formats (xmplay + openmpt supported)
static const std::set<std::string> allFormats = {
    "mptm","mod","s3m","xm","it","669","amf","ams","c67","dbm","digi","dmf","dsm","dsym","dtm","far","fmt","imf","ice","j2b","m15","mdl","med","mms","mt2","mtm","nst","okt","plm","psm","pt36","ptm","sfx","sfx2","st26","stk","stm","stx","stp","symmod","ult","wow","gdm","mo3","oxm","umx","xpk","ppm","mmcmp", "mmd0", "mmd1", "mmd2", "mmd3", "mmdc"
};

struct RebuildParams {
    HWND    hDlg;
    bool all;
    // HINSTANCE hInst;
    // XMPFUNC_MISC* xmpfmisc;
};

static const char* g_colNames[] = {
    "extension",  // columna 0
    "artist",     // columna 1
    "song",       // columna 2
    "size",
    "full_path"   // columna 3
};
static int  g_sortColumn = -1;   // -1 = sin ordenar todavía
static bool g_sortAsc = true; // sentido actual

struct SortData {
    HWND  hList;
    int   column;
    bool  asc;
};

struct CtrlInfo {
    RECT  rc;        // rect original en coords de cliente
    bool  moveX;     // si debe moverse horizontalmente
    bool  moveY;     // si debe moverse verticalmente
    bool  sizeW;     // si debe cambiar de ancho
    bool  sizeH;     // si debe cambiar de alto
};
static RECT               g_rcInitClient;
static std::map<int, CtrlInfo> g_mapCtrls;

static bool g_use_pls = true;  // default if no config.ini is present

// -- config, Size, URL encode and helpers

static bool LoadConfigIni()
{
    // locate config.ini next to DLL
    wchar_t dllDir[MAX_PATH]{};
    if (!GetModuleFileNameW(hInstance, dllDir, MAX_PATH))
        return false;
    PathRemoveFileSpecW(dllDir);

    wchar_t iniPath[MAX_PATH]{};
    if (!PathCombineW(iniPath, dllDir, L"cmod.ini"))
        return false;

    // If it doesn't exist, keep defaults
    const DWORD attr = GetFileAttributesW(iniPath);
    if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY))
        return false;

    // Read use_pls under [cmod]
    // (GetPrivateProfileIntW is simpler/safer than parsing a string)
    const int use_pls = GetPrivateProfileIntW(L"cmod", L"use_pls", /*default*/1, iniPath);
    g_use_pls = (use_pls != 0);
    return true;
}

static void ClearSelectionNow() {
    xmpfmisc->DDE("key341");
    xmpfmisc->DDE("key342");
}

static std::wstring HumanSize(sqlite3_int64 bytes)
{
    const wchar_t* units[] = { L"B", L"KB", L"MB", L"GB", L"TB" };
    double size = static_cast<double>(bytes);
    int unit = 0;
    while (size >= 1024.0 && unit < 4) {
        size /= 1024.0;
        ++unit;
    }
    wchar_t buf[64];
    // swprintf_s is the MSVC‐safe version of swprintf
    swprintf_s(buf, _countof(buf), L"%.1f %s", size, units[unit]);
    return buf;
}

inline std::wstring get_window_textW(HWND h) {
    int len = GetWindowTextLengthW(h);
    std::wstring w(len + 1, L'\0');              // espacio para el NUL
    int got = GetWindowTextW(h, &w[0], len + 1); // got no incluye el NUL
    if (got < 0) got = 0;
    w.resize(got);                                // quita el NUL
    return w;
}

// -- DB build info storage/retrieval

// Guarda metadatos de build en la BBDD abierta 'db'
static void StoreDbBuildInfo(sqlite3* db, bool allFormats)
{
    sqlite3_exec(db,
        "CREATE TABLE IF NOT EXISTS meta("
        "  k TEXT PRIMARY KEY,"
        "  v TEXT NOT NULL"
        ");", nullptr, nullptr, nullptr);

    const time_t now = std::time(nullptr);
    const std::string nowStr = std::to_string((long long)now);
    const std::string isoStr = to_iso8601_utc(now);
    const char* kind = allFormats ? "all" : "default";

    sqlite3_stmt* st = nullptr;
    const char* sql =
        "INSERT OR REPLACE INTO meta(k,v) VALUES "
        " ('built_unix', ?1),"
        " ('built_iso',  ?2),"
        " ('build_kind', ?3);";
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) == SQLITE_OK) {
        sqlite3_bind_text(st, 1, nowStr.c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(st, 2, isoStr.c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(st, 3, kind, -1, SQLITE_TRANSIENT);
        sqlite3_step(st);
    }
    sqlite3_finalize(st);

    // refresca la caché en memoria
    g_dbBuiltUnix = now;
    g_dbBuiltIsoW = to_wide(isoStr);
    g_dbBuiltAllFormats = allFormats;
}

// Carga metadatos de build desde 'db'; si no hay tabla meta, cae al mtime del fichero
static void LoadDbBuildInfo(sqlite3* db, const std::wstring& dbPath)
{
    g_dbBuiltUnix = 0;
    g_dbBuiltIsoW.clear();
    g_dbBuiltAllFormats = false;

    auto getMeta = [&](const char* key) -> std::string {
        sqlite3_stmt* st = nullptr;
        std::string val;
        if (sqlite3_prepare_v2(db, "SELECT v FROM meta WHERE k=?1;", -1, &st, nullptr) == SQLITE_OK) {
            sqlite3_bind_text(st, 1, key, -1, SQLITE_TRANSIENT);
            if (sqlite3_step(st) == SQLITE_ROW) {
                const unsigned char* p = sqlite3_column_text(st, 0);
                if (p) val.assign((const char*)p);
            }
        }
        sqlite3_finalize(st);
        return val;
    };

    // ¿existe tabla meta?
    bool hasMeta = false;
    {
        sqlite3_stmt* st = nullptr;
        if (sqlite3_prepare_v2(db,
            "SELECT 1 FROM sqlite_master WHERE type='table' AND name='meta';",
            -1, &st, nullptr) == SQLITE_OK) {
            hasMeta = (sqlite3_step(st) == SQLITE_ROW);
        }
        sqlite3_finalize(st);
    }

    if (hasMeta) {
        std::string u = getMeta("built_unix");
        std::string i = getMeta("built_iso");
        std::string k = getMeta("build_kind");
        if (!u.empty()) g_dbBuiltUnix = (time_t)_strtoi64(u.c_str(), nullptr, 10);
        if (!i.empty()) g_dbBuiltIsoW = to_wide(i);
        g_dbBuiltAllFormats = (_stricmp(k.c_str(), "all") == 0);
    }
    else {
        // Fallback: mtime del archivo
        time_t t = file_mtime_utc(dbPath);
        if (t) {
            g_dbBuiltUnix = t;
            g_dbBuiltIsoW = to_wide(to_iso8601_utc(t));
            g_dbBuiltAllFormats = false;
        }
    }
}

// -- DB rebuild

static bool RebuildDatabase(XMPFILE txtFile,
    const std::wstring& dbPathW, bool all)
{
    // Remove any old DB
    ::DeleteFileW(dbPathW.c_str());

    // Open new DB
    sqlite3* db = nullptr;
    if (sqlite3_open16(dbPathW.c_str(), &db) != SQLITE_OK)
        return false;

    sqlite3_exec(db, "PRAGMA encoding='UTF-8';", nullptr, nullptr, nullptr);

    // Speed tweaks: in-memory journal + no fsync
    sqlite3_exec(db, "PRAGMA journal_mode = MEMORY;", nullptr, nullptr, nullptr);
    sqlite3_exec(db, "PRAGMA synchronous = OFF;", nullptr, nullptr, nullptr);

    // Create FTS5 table
    const char* ddl =
        "CREATE VIRTUAL TABLE modland USING fts5("
        " tracker, extension, artist, song, full_path, size UNINDEXED"
        ");";
    if (sqlite3_exec(db, ddl, nullptr, nullptr, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }

    // Begin one big transaction
    sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);

    // Prepare insert statement once
    sqlite3_stmt* ins = nullptr;
    const char* sql = "INSERT INTO modland VALUES (?1,?2,?3,?4,?5,?6);";
    if (sqlite3_prepare_v2(db, sql, -1, &ins, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }

    // TODO allowed or allowedAncient
    const std::set<std::string> allowed = all ? allFormats : xmplayFormats;

    const size_t CHUNK = 16 * 1024;
    std::vector<char> chunkBuf(CHUNK);
    std::string carry;  // holds partial line across reads

    while (true) {
        int bytes = xmpffile->Read(txtFile, chunkBuf.data(), CHUNK);
        if (bytes <= 0) break;
        carry.append(chunkBuf.data(), bytes);

        size_t pos;
        while ((pos = carry.find('\n')) != std::string::npos) {
            std::string line8 = carry.substr(0, pos);
            carry.erase(0, pos + 1);

            // trim trailing '\r'
            if (!line8.empty() && line8.back() == '\r')
                line8.pop_back();

            // convert UTF-8 → UTF-16
            std::wstring lineW = to_wide(line8);
            if (lineW.empty()) continue;

            // --- 4) Your existing parsing + binding logic: ---
            auto tab = lineW.find(L'\t');
            if (tab == std::wstring::npos) continue;
            std::wstring wsiz = lineW.substr(0, tab);
            std::wstring rest = lineW.substr(tab + 1);

            auto dot = rest.rfind(L'.');
            if (dot == std::wstring::npos) continue;
            std::string ext = to_utf8(rest.substr(dot + 1));
            for (auto& ch : ext) ch = (char)std::tolower((unsigned char)ch);
            if (!allowed.count(ext)) continue;

            auto firstSlash = rest.find(L'/');
            auto lastSlash = rest.rfind(L'/');
            if (firstSlash == std::wstring::npos ||
                lastSlash == firstSlash) continue;

            std::wstring tracker = rest.substr(0, firstSlash);
            std::wstring artist = rest.substr(firstSlash + 1,
                lastSlash - firstSlash - 1);
            std::wstring song = rest.substr(lastSlash + 1);

            sqlite3_bind_text(ins, 1, to_utf8(tracker).c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_text(ins, 2, ext.c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_text(ins, 3, to_utf8(artist).c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_text(ins, 4, to_utf8(song).c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_text(ins, 5, to_utf8(rest).c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_int64(ins, 6, std::wcstoll(wsiz.c_str(), nullptr, 10));

            sqlite3_step(ins);
            sqlite3_reset(ins);
        }
    }

    StoreDbBuildInfo(db, /*allFormats=*/all);

    // Finish up
    sqlite3_finalize(ins);
    sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
    sqlite3_close(db);


    return true;
}

// Closes the old g_db, renames cmod_new.db → cmod.db, and re-opens g_db
static bool SwapInNewDatabase()
{
    // 1) Compute paths
    wchar_t modulePath[MAX_PATH];
    GetModuleFileNameW(hInstance, modulePath, MAX_PATH);
    PathRemoveFileSpecW(modulePath);
    std::wstring dir = modulePath;

    std::wstring oldDb = dir + L"\\cmod.db";
    std::wstring newDb = dir + L"\\cmod_new.db";

    // 2) Close old handle
    if (g_db) {
        sqlite3_close(g_db);
        g_db = nullptr;
    }

    // 3) Replace files on disk
    if (!::MoveFileExW(newDb.c_str(), oldDb.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
        DWORD err = ::GetLastError();
        wchar_t buf[128];
        swprintf_s(buf, L"MoveFileW failed with 0x%08X", err);
        MessageBoxW(NULL, buf, L"SwapInNewDatabase", MB_OK | MB_ICONERROR);
        return false;
    }

    // 4) Re-open into g_db
    std::string oldDbUtf8 = to_utf8(oldDb);
    int rc = sqlite3_open_v2(
        oldDbUtf8.c_str(),
        &g_db,
        SQLITE_OPEN_READONLY,
        nullptr
    );

    if (rc != SQLITE_OK) {
        // error -> mensaje
        const char* errA = sqlite3_errmsg(g_db);
        std::wstring errW = to_wide(errA);

        std::wstring msg = L"Cannot reopen DB in:\n";
        msg += oldDb;
        msg += L"\n\nSQLite error:\n";
        msg += errW;

        MessageBoxW(NULL, msg.c_str(), L"SQLite Error", MB_OK | MB_ICONERROR);
        sqlite3_close(g_db);
        g_db = nullptr;
        return false;
    }

    // ::DeleteFileW(zip.c_str());
    // ::DeleteFileW(txt.c_str());

    // Refresca caché de metadatos tras el swap
    LoadDbBuildInfo(g_db, oldDb);

    return true;
}

static unsigned __stdcall RebuildThread(void* pv)
{
    auto* p = static_cast<RebuildParams*>(pv);
    HWND  hDlg = p->hDlg;
    bool all = p->all;

    // 1) Determinar paths
    wchar_t modulePath[MAX_PATH];
    GetModuleFileNameW(hInstance, modulePath, MAX_PATH);
    PathRemoveFileSpecW(modulePath);
    std::wstring dir = modulePath;
    std::wstring newDb = dir + L"\\cmod_new.db";

    bool success = false;

    // 2) Descarga
    xmpfmisc->ShowBubble("Downloading allmods...", 1000);
    XMPFILE txtFile = xmpffile->Open("https://modland.com/allmods.zip|allmods.txt");
    if (txtFile) {
        // read and process file here
        xmpfmisc->ShowBubble("Rebuilding DB…", 1000);
        if (RebuildDatabase(txtFile, newDb, all))
        {
            // 5) Swap
            xmpfmisc->ShowBubble("Swapping in new DB...", 1000);
            if (SwapInNewDatabase())
            {
                success = true;
            }
        }

        xmpffile->Close(txtFile);
    }

    // 6) Notificar resultado y liberar memoria
    PostMessageW(hDlg, WM_DB_REBUILT, success ? TRUE : FALSE, 0);
    delete p;
    return 0;
}

// ---- Implementación ----

static void WINAPI Plugin_About(HWND parent) {
    MessageBoxW(
        parent,
        L"This plugin does not need to be enabled in the plugins options.\n\n"
        L"To use it, assign a shortcut key in XMPlay\n"
        L"and use it to open the search dialog.",
        L"cmod (modland) - Information",
        MB_OK | MB_ICONINFORMATION
    );
}

static void *WINAPI Plugin_Init(void) {
    return nullptr;
}

static void WINAPI Plugin_Exit(void* inst) {
    // Cerrar BD, detener hilos, etc.
    if (g_db) {
        sqlite3_close(g_db);
        g_db = nullptr;
    }
}

static const char* WINAPI Plugin_GetDescription(void* inst) {
    return "cmod (modland) by herotyc";
}

static void WINAPI MyConfig(void* inst, HWND win) {
    /*DialogBoxParam(
        (HINSTANCE)g_faceproc(XMPFUNC_WINDOW_FACE),
        MAKEINTRESOURCE(IDD_SEARCH),
        win,
        SearchDlgProc,
        0
    );*/
}

static DWORD WINAPI Plugin_GetConfig(void* inst, void* config) {
    // Tamaño de PluginConfig
    // if (config) memcpy(config, &(PluginConfig){}, sizeof(PluginConfig));
    return sizeof(PluginConfig);
}

static BOOL WINAPI Plugin_SetConfig(void* inst, void* config, DWORD size) {
    // Asignar config si guardas algo
    return TRUE;
}

static LRESULT CALLBACK EditSubclassProc(
    HWND    hWnd,
    UINT    msg,
    WPARAM  wParam,
    LPARAM  lParam,
    UINT_PTR uIdSubclass,
    DWORD_PTR dwRefData
) {
    switch (msg) {
    case WM_GETDLGCODE:
        // Queremos ENTER, ESC y el resto de teclas de diálogo
        return DLGC_WANTALLKEYS;

    case WM_KEYDOWN:
        if (wParam == VK_RETURN) {
            // We use the debounce timer to do the search, but you can do it manually still
            DoSearch(GetParent(hWnd));
            return 0;  // come la tecla y no deje beep
        }
        if (wParam == VK_ESCAPE) {
            // cierra el diálogo con IDCANCEL
            EndDialog(GetParent(hWnd), IDCANCEL);
            return 0;  // come la tecla
        }
        break;

    case WM_CHAR:
        if (wParam == '\r' || wParam == '\x1B') {
            // come tanto RETURN como ESC para evitar el beep
            return 0;
        }
        break;
    }

    return DefSubclassProc(hWnd, msg, wParam, lParam);
}

static bool EnsureDatabaseOpen(HWND hDlg)
{
    if (g_db)
        return true;

    // Compute path to cmod.db
    wchar_t modulePath[MAX_PATH];
    GetModuleFileNameW(hInstance, modulePath, MAX_PATH);
    PathRemoveFileSpecW(modulePath);
    std::wstring dbPath = std::wstring(modulePath) + L"\\cmod.db";

    // UTF-8 for sqlite
    std::string dbUtf8 = to_utf8(dbPath);

    // Try opening read-only
    int rc = sqlite3_open_v2(
        dbUtf8.c_str(),
        &g_db,
        SQLITE_OPEN_READONLY,
        nullptr
    );
    if (rc == SQLITE_OK) {
        LoadDbBuildInfo(g_db, dbPath);
        return true;
    }

    // On error, pull the UTF-8 msg, convert, and show
    const char* errA = sqlite3_errmsg(g_db);
    std::wstring errW = to_wide(errA);   // convierte ANTES de cerrar
    sqlite3_close(g_db);
    g_db = nullptr;
    
    std::wstring msg = L"Error opening database:\n" + dbPath +
        L"\n\nSQLite error:\n" + errW + 
        L"\n\nClick 'Check for updates' and rebuild the db, wait a moment, and try again.";
    MessageBoxW(hDlg, msg.c_str(), L"SQLite Error", MB_OK | MB_ICONERROR);

    return false;
}

// -- Helpers para construir el MATCH ------------------------------

static std::wstring trim_ws(const std::wstring& s) {
    size_t i = 0, j = s.size();
    while (i < j && iswspace(s[i])) ++i;
    while (j > i && iswspace(s[j - 1])) --j;
    return s.substr(i, j - i);
}

// Convierte puntuación en espacios y compone "t1* t2* ...".
static std::string BuildMatchFromFreeText(const std::wstring& wquery) {
    // 1) puntuación -> espacio (conserva letras/dígitos/_)
    std::wstring norm; norm.reserve(wquery.size());
    for (wchar_t ch : wquery) {
        if (iswalnum(ch) || ch == L'_') norm.push_back(ch);
        else norm.push_back(L' ');
    }

    // 2) split por espacios
    std::vector<std::wstring> terms;
    size_t i = 0, n = norm.size();
    while (i < n) {
        while (i < n && iswspace(norm[i])) ++i;
        size_t j = i;
        while (j < n && !iswspace(norm[j])) ++j;
        if (j > i) terms.emplace_back(norm.substr(i, j - i));
        i = j;
    }

    // 3) "term*" en UTF-8
    std::string q;
    for (auto& t : terms) {
        std::string u8 = to_utf8(t);
        if (u8.empty()) continue;
        if (!q.empty()) q.push_back(' ');
        q += u8;
        if (u8.back() != '*') q.push_back('*');
    }
    return q; // puede quedar vacío si todo era puntuación
}

static void DoSearch(HWND hDlg, bool exact, int randomCount) {
    if (!EnsureDatabaseOpen(hDlg))
        return;    // if it failed, we already showed an error

    // 0) Actualizar title bar
    std::wstring title = L"cmod";

    // obtener 'format' del combo
    static const wchar_t* formatNames[] = { L"Any", L"IT", L"XM", L"S3M", L"MOD" };
    int fmtIdx = (int)SendMessageW(
        GetDlgItem(hDlg, IDC_COMBO_FORMAT),
        CB_GETCURSEL, 0, 0
    );
    const wchar_t* fmtName = (fmtIdx >= 0 && fmtIdx < 5)
        ? formatNames[fmtIdx]
        : L"Any";
    if (randomCount > 0) {
        title = title
            + std::wstring(L" – random ") + std::to_wstring(randomCount) + L" songs";
        title += L" [";
        title += fmtName;
        title += L"]";
    }
    else {
        wchar_t buf[256];
        GetDlgItemTextW(hDlg, IDC_EDIT_SEARCH, buf, _countof(buf));
        std::wstring search(buf);
        if (search.length() >= 3) {
            title += L" - search: ";
            if (search.size() > 30)
                title += search.substr(0, 27) + L"...";
            else
                title += search;
        }
        static const wchar_t* searchByNames[] = { L"Artist", L"Song", L"All" };
        int byIdx = (int)SendMessageW(
            GetDlgItem(hDlg, IDC_COMBO_SEARCH),
            CB_GETCURSEL, 0, 0
        );
        const wchar_t* byName = (byIdx >= 0 && byIdx < 3)
            ? searchByNames[byIdx]
            : L"All";

        //   c) concatenar entre corchetes
        title += L" [";
        title += byName;
        title += L", ";
        title += fmtName;
        title += L"]";
    }
    title += DbBadge();
    SetWindowTextW(hDlg, title.c_str());

    HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);
    ListView_DeleteAllItems(hList);

    // 2) Determinar columna a usar
    HWND hComboSearch = GetDlgItem(hDlg, IDC_COMBO_SEARCH);
    int sel = (int)SendMessageW(hComboSearch, CB_GETCURSEL, 0, 0);
    const char* column;
    switch (sel) {
    case 0:  column = "artist"; break;
    case 1:  column = "song"; break;
    case 2:  column = "modland";   break;
    default: column = "modland"; // All: toda la tabla FTS5
    }

    HWND hComboFormat = GetDlgItem(hDlg, IDC_COMBO_FORMAT);
    sel = (int)SendMessageW(hComboFormat, CB_GETCURSEL, 0, 0);
    const char* format;
    switch (sel) {
    case 0:  format = "any"; break;
    case 1:  format = "it"; break;
    case 2:  format = "xm";   break;
    case 3:  format = "s3m"; break;
    case 4:  format = "mod";   break;
    default: format = "any"; // All: toda la tabla FTS5
    }

    sqlite3_stmt* stmt = nullptr;
    if (randomCount > 0) {
        // ---- RANDOM SONGS PATH ----
        bool hasFilter = (std::string(format) != "any");

        // Build the SQL
        std::string sql =
            "SELECT UPPER(extension), artist, song, size, full_path "
            "FROM modland";
        if (hasFilter) {
            sql += " WHERE extension = ?1";
            sql += " ORDER BY random() LIMIT ?2;";
        }
        else {
            sql += " ORDER BY random() LIMIT ?1;";
        }

        if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
            std::wstring wtitle = L"SQLite prepare (random)";
            std::wstring wmsg = to_wide(sqlite3_errmsg(g_db));
            MessageBoxW(NULL, wmsg.c_str(), wtitle.c_str(), MB_OK | MB_ICONERROR);
            return;
        }
        if (hasFilter) {
            // 1st param = format string
            sqlite3_bind_text(stmt, 1, format, -1, SQLITE_TRANSIENT);
            // 2nd param = count limit
            sqlite3_bind_int(stmt, 2, randomCount);
        }
        else {
            // Only one param: limit
            sqlite3_bind_int(stmt, 1, randomCount);
        }
    }
    else {
        // 1) Leer búsqueda (UTF-16) y convertir a UTF-8 + wildcard
        // only append ‘*’ if
        //  1) the user didn’t request exact,
        //  2) they haven’t wrapped the query in quotes,
        //  3) and it doesn’t already end with ‘*’
        std::wstring wraw = get_window_textW(GetDlgItem(hDlg, IDC_EDIT_SEARCH));
        std::wstring wq = trim_ws(wraw);

        // ¿Usuario ha escrito comillas?
        bool userQuoted = (wq.size() >= 2 && wq.front() == L'"' && wq.back() == L'"');

        std::string matchParam;
        if (exact || userQuoted) {
            // Frase exacta: quita comillas exteriores si vienen, escapa " internos y envuelve.
            std::wstring inner = userQuoted ? wq.substr(1, wq.size() - 2) : wq;

            std::wstring escaped; escaped.reserve(inner.size());
            for (wchar_t c : inner) {
                if (c == L'"') {
                    escaped.push_back(L'"'); escaped.push_back(L'"');
                }
                else {
                    escaped.push_back(c);
                }
            }
            std::string utf8 = to_utf8(escaped);
            matchParam.reserve(utf8.size() + 2);
            matchParam.push_back('"');
            matchParam += utf8;
            matchParam.push_back('"');
        }
        else {
            // Libre: sanea y usa prefijos por término
            matchParam = BuildMatchFromFreeText(wq);
        }

        // Si tras sanear quedó vacío, sal con un mensajito
        if (matchParam.empty()) {
            SetDlgItemTextW(hDlg, IDC_STATIC_COUNT, L"Type at least 3 letters");
            if (stmt) sqlite3_finalize(stmt);
            return;
        }

        // 2) Preparar la consulta FTS5 con ORDER BY artist, bm25
        std::string orderBy;

        // ¿Hay una columna activa para ordenar?
        if (g_sortColumn >= 0 && g_sortColumn < 4) {
            // p.ej. "artist COLLATE NOCASE DESC"
            orderBy = std::string(g_colNames[g_sortColumn])
                + " COLLATE NOCASE "
                + (g_sortAsc ? "ASC" : "DESC")
                + ", bm25(modland) ASC";
        }
        else {
            // default si no se ha pinchado ninguna columna
            orderBy = "artist COLLATE NOCASE ASC, bm25(modland) ASC";
        }

        // Monta el SQL con ORDER BY dinámico
        std::string sql =
            "SELECT upper(extension), artist, song, size, full_path "
            "FROM modland "
            "WHERE " + std::string(column) + " MATCH ?1 ";

        if (std::string(format) != "any") {
            sql += "AND extension = ?2 ";
        }

        sql +=
            "ORDER BY " + orderBy + " "
            "LIMIT 1000;";

        int rc = sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr);
        if (rc != SQLITE_OK) {
            const char* errMsg = sqlite3_errmsg(g_db);

            std::string fullMsg;
            fullMsg.reserve(sql.size() + strlen(errMsg) + 64);
            fullMsg = "When preparing the query:\n";
            fullMsg += sql;
            fullMsg += "\n\nSQLite error:\n";
            fullMsg += errMsg;

            std::wstring wtitle = L"SQLite error";
            std::wstring wfull = to_wide(fullMsg);
            MessageBoxW(NULL, wfull.c_str(), wtitle.c_str(), MB_OK | MB_ICONERROR);

            if (stmt) sqlite3_finalize(stmt);

            return;
        }


        // 3) Binder parámetro
        sqlite3_bind_text(stmt, 1, matchParam.c_str(), -1, SQLITE_TRANSIENT);

        if (std::string(format) != "any") {
            sqlite3_bind_text(stmt, 2, format, -1, SQLITE_TRANSIENT);
        }
    }

    // 4) Recorrer resultados e insertarlos en el ListView
    int index = 0;
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        const std::wstring wExt = wide_from_sqlite(stmt, 0);
        const std::wstring wArtist = wide_from_sqlite(stmt, 1);
        const std::wstring wSong = wide_from_sqlite(stmt, 2);
        sqlite3_int64 sz = sqlite3_column_int64(stmt, 3);
        const std::wstring wPath = wide_from_sqlite(stmt, 4);

        // 4.1) Insertar columna 0 (extension)
        // Insertamos la fila (col 0) con el texto propio
        LVITEMW item{};
        item.mask = LVIF_TEXT | LVIF_PARAM;
        item.iItem = index;
        item.iSubItem = 0;
        item.pszText = const_cast<LPWSTR>(wExt.c_str());
        item.lParam = (LPARAM)(sz);

        int row = ListView_InsertItem(hList, &item);
        if (row >= 0) {
            ListView_SetItemText(hList, row, 1, const_cast<LPWSTR>(wArtist.c_str()));
            ListView_SetItemText(hList, row, 2, const_cast<LPWSTR>(wSong.c_str()));
            std::wstring human = HumanSize(sz);
            ListView_SetItemText(hList, row, 3, const_cast<LPWSTR>(human.c_str()));
            ListView_SetItemText(hList, row, 4, const_cast<LPWSTR>(wPath.c_str()));
        }

        ++index;
    }

    sqlite3_finalize(stmt);

    int count = ListView_GetItemCount(hList);
    // 1) Construye un std::wstring que contenga tu texto
    std::wstring txt =
        std::to_wstring(count)
        + (count == 1 ? L" result" : L" results");

    // 2) Y pásalo a SetDlgItemTextW, que espera LPCWSTR
    SetDlgItemTextW(hDlg, IDC_STATIC_COUNT, txt.c_str());
}

static void DoRandomArtist(HWND hDlg)
{
    if (!EnsureDatabaseOpen(hDlg))
        return;    // if it failed, we already showed an error

    // 1) Count distinct artists
    int total = 0;
    {
        sqlite3_stmt* stmt = nullptr;
        if (sqlite3_prepare_v2(g_db,
            "SELECT COUNT(DISTINCT artist) FROM modland;", -1, &stmt, nullptr) == SQLITE_OK)
        {
            if (sqlite3_step(stmt) == SQLITE_ROW)
                total = sqlite3_column_int(stmt, 0);
        }
        sqlite3_finalize(stmt);
    }

    if (total <= 0) return;

    // 2) Pick a random index
    int idx = std::rand() % total;

    // 3) Fetch the artist at that offset
    std::wstring artistStr;                // <-- our own copy
    {
        sqlite3_stmt* stmt = nullptr;
        if (sqlite3_prepare_v2(
            g_db,
            "SELECT DISTINCT artist FROM modland LIMIT 1 OFFSET ?1;",
            -1, &stmt, nullptr
        ) == SQLITE_OK)
        {
            sqlite3_bind_int(stmt, 1, idx);
            if (sqlite3_step(stmt) == SQLITE_ROW)
            {
                // Grab the UTF-16 LE blob
                const std::wstring art = wide_from_sqlite(stmt, 0);
                if (!art.empty()) artistStr = L"\"" + art + L"\"";
            }
        }
        sqlite3_finalize(stmt);
    }

    if (artistStr.empty())
    {
        MessageBoxW(hDlg,
            L"No artist found at that offset!",
            L"Debug", MB_OK);
        return;
    }

    // Debug: show the artist we copied
    /*MessageBoxW(hDlg, artistStr.c_str(),
        L"Debug Artist (copied UTF-16)", MB_OK);*/

    // 4) Stuff into the UI and re-run DoSearch
    SetDlgItemTextW(hDlg, IDC_EDIT_SEARCH, artistStr.c_str());
    SendMessageW(GetDlgItem(hDlg, IDC_COMBO_SEARCH),
        CB_SETCURSEL, 0 /*Artist*/, 0);
    SendMessageW(
        GetDlgItem(hDlg, IDC_COMBO_FORMAT),
        CB_SETCURSEL, 0 /* Any */, 0);
    DoSearch(hDlg, /*exact=*/true);
}


static int CALLBACK ListCompare(LPARAM lhs, LPARAM rhs, LPARAM ctx)
{
    const SortData* sd = reinterpret_cast<const SortData*>(ctx);
    const HWND hList = sd->hList;
    const int  col = sd->column;
    const bool asc = sd->asc;

    auto inv = [&](int c) { return asc ? c : -c; };

    if (col == 3) { // Size: usa lParam como clave numérica
        LVITEMW it{};
        it.mask = LVIF_PARAM;

        it.iItem = (int)lhs;
        ListView_GetItem(hList, &it);
        long long s1 = static_cast<long long>(it.lParam);

        it.iItem = (int)rhs;
        ListView_GetItem(hList, &it);
        long long s2 = static_cast<long long>(it.lParam);

        if (s1 < s2) return asc ? -1 : 1;
        if (s1 > s2) return asc ? 1 : -1;

        // desempates estables: Artist, Song
        WCHAR a1[256] = L"", a2[256] = L"";
        ListView_GetItemText(hList, (int)lhs, 1, a1, _countof(a1));
        ListView_GetItemText(hList, (int)rhs, 1, a2, _countof(a2));
        int c = _wcsicmp(a1, a2);
        if (c) return inv(c);

        WCHAR sA[256] = L"", sB[256] = L"";
        ListView_GetItemText(hList, (int)lhs, 2, sA, _countof(sA));
        ListView_GetItemText(hList, (int)rhs, 2, sB, _countof(sB));
        return inv(_wcsicmp(sA, sB));
    }

    // Texto: compara el subitem 'col' case-insensitive
    WCHAR t1[512] = L"", t2[512] = L"";
    ListView_GetItemText(hList, (int)lhs, col, t1, _countof(t1));
    ListView_GetItemText(hList, (int)rhs, col, t2, _countof(t2));
    int cmp = _wcsicmp(t1, t2);
    if (cmp == 0 && col != 2) {
        // desempate por Song para que el orden sea estable/agradable
        WCHAR s1[512] = L"", s2[512] = L"";
        ListView_GetItemText(hList, (int)lhs, 2, s1, _countof(s1));
        ListView_GetItemText(hList, (int)rhs, 2, s2, _countof(s2));
        cmp = _wcsicmp(s1, s2);
    }
    return inv(cmp);
}


static BOOL CALLBACK SearchDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_INITDIALOG:
    {
        // 1) Captura tamaño cliente inicial
        GetClientRect(hDlg, &g_rcInitClient);

        // 2) Define qué controles y cómo se anclan/expanden
        struct Def { int id; bool mX, mY, sW, sH; };
        Def a[] = {
            { IDC_LIST_RESULTS, false,  false,  true,  true },   // lista se expande en todo
            { IDC_EDIT_SEARCH,  false,false, true,  false},   // edit solo crece en ancho
            { IDC_COMBO_SEARCH, true,  false, false, false},  // comboSearch se mueve en X
            { IDC_STATIC_SEARCHBY,true,false,false, false},   // idem label
            { IDC_COMBO_FORMAT,true,  false, false, false},   // comboFormat se mueve en X
            { IDC_STATIC_FORMAT,true,false,false, false},     // label format
            { IDC_BUTTON_SONGS,true, false, false, false},
            { IDC_COMBO_NUMBER,true, false, false, false},
            { IDC_BUTTON_ADD_ALL,false,true, false, false},   // botón ADD se mueve en Y
            { IDC_BUTTON_REPLACE_ALL, false,  true,  false, false},   // botón REPLACE idem
            { IDC_STATIC_COUNT, false,  true,  false, false},  // contador se mueve X e Y
            { IDC_BUTTON_CHECK_UPDATES, true, true, false, false}, // botón CHECK se mueve en Y
        };

        for (auto& d : a) {
            HWND h = GetDlgItem(hDlg, d.id);
            RECT r;
            GetWindowRect(h, &r);
            ScreenToClient(hDlg, (LPPOINT)&r);
            ScreenToClient(hDlg, ((LPPOINT)&r) + 1);
            g_mapCtrls[d.id] = { r, d.mX, d.mY, d.sW, d.sH };
        }

        // 1) Inicializar ListView
        INITCOMMONCONTROLSEX icex = { sizeof(icex), ICC_LISTVIEW_CLASSES };
        InitCommonControlsEx(&icex);

        // make the dialog itself clip its children
        LONG_PTR dlgStyle = GetWindowLongPtr(hDlg, GWL_STYLE);
        SetWindowLongPtr(hDlg, GWL_STYLE, dlgStyle | WS_CLIPCHILDREN);

        SetDlgItemTextW(hDlg, IDC_STATIC_COUNT, L"0 results");

        HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);

        // 1) Clear the virtual-list style and ensure REPORT mode:
        LONG_PTR style = GetWindowLongPtr(hList, GWL_STYLE);
        style &= ~LVS_OWNERDATA;   // remove owner-data (virtual) bit
        style |= LVS_REPORT;      // make sure it’s report view
        SetWindowLongPtr(hList, GWL_STYLE, style | WS_CLIPSIBLINGS);

        // 2) Now set your extended styles:
        DWORD ex = ListView_GetExtendedListViewStyle(hList);
        ex |= LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER;
        ListView_SetExtendedListViewStyle(hList, ex);

        LVCOLUMN col = { 0 };
        col.mask = LVCF_TEXT | LVCF_WIDTH;
        col.cx = 40; col.pszText = const_cast<LPWSTR>(TEXT("Ext"));        ListView_InsertColumn(hList, 0, &col); g_colInitWidth[0] = col.cx;
        col.cx = 120; col.pszText = const_cast<LPWSTR>(TEXT("Artist"));    ListView_InsertColumn(hList, 1, &col); g_colInitWidth[1] = col.cx;
        col.cx = 140; col.pszText = const_cast<LPWSTR>(TEXT("Song"));      ListView_InsertColumn(hList, 2, &col); g_colInitWidth[2] = col.cx;
        col.cx = 80; col.pszText = const_cast<LPWSTR>(TEXT("Size"));      ListView_InsertColumn(hList, 3, &col); g_colInitWidth[3] = col.cx;
        col.cx = 300; col.pszText = const_cast<LPWSTR>(TEXT("Full Path")); ListView_InsertColumn(hList, 4, &col); g_colInitWidth[4] = col.cx;

        // 2) Subclasificar el EDIT para detectar Enter
        HWND hEdit = GetDlgItem(hDlg, IDC_EDIT_SEARCH);
        SendMessageW(hEdit, EM_SETCUEBANNER, (WPARAM)TRUE,
            (LPARAM)L"Type 3 or more chars...");
        SetWindowSubclass(hEdit, EditSubclassProc, 0, 0);
        SetFocus(hEdit);

        // 3) Inicializa el ComboBox de campo
        HWND hComboSearch = GetDlgItem(hDlg, IDC_COMBO_SEARCH);
        SendMessageW(hComboSearch, CB_RESETCONTENT, 0, 0);
        SendMessageW(hComboSearch, CB_ADDSTRING, 0, (LPARAM)L"Artist");
        SendMessageW(hComboSearch, CB_ADDSTRING, 0, (LPARAM)L"Song");
        SendMessageW(hComboSearch, CB_ADDSTRING, 0, (LPARAM)L"All");
        SendMessageW(hComboSearch, CB_SETCURSEL, 0, 0);  // por defecto "Artist"

        // 3) Inicializa el ComboBox de campo
        HWND hComboFormat = GetDlgItem(hDlg, IDC_COMBO_FORMAT);
        SendMessageW(hComboFormat, CB_RESETCONTENT, 0, 0);
        SendMessageW(hComboFormat, CB_ADDSTRING, 0, (LPARAM)L"Any");
        SendMessageW(hComboFormat, CB_ADDSTRING, 0, (LPARAM)L"IT");
        SendMessageW(hComboFormat, CB_ADDSTRING, 0, (LPARAM)L"XM");
        SendMessageW(hComboFormat, CB_ADDSTRING, 0, (LPARAM)L"S3M");
        SendMessageW(hComboFormat, CB_ADDSTRING, 0, (LPARAM)L"MOD");
        SendMessageW(hComboFormat, CB_SETCURSEL, 0, 0);  // por defecto "Any"

        HWND hComboCount = GetDlgItem(hDlg, IDC_COMBO_NUMBER);
        SendMessageW(hComboCount, CB_RESETCONTENT, 0, 0);
        SendMessageW(hComboCount, CB_ADDSTRING, 0, (LPARAM)L"10");
        SendMessageW(hComboCount, CB_ADDSTRING, 0, (LPARAM)L"50");
        SendMessageW(hComboCount, CB_ADDSTRING, 0, (LPARAM)L"100");
        SendMessageW(hComboCount, CB_ADDSTRING, 0, (LPARAM)L"500");
        SendMessageW(hComboCount, CB_ADDSTRING, 0, (LPARAM)L"1000");

        // 2) Optionally set a default selection (e.g. “100”)
        SendMessageW(hComboCount, CB_SETCURSEL, 2 /* zero‐based index*/, 0);

        // Load our plugin’s icon from resources
        HICON hIconSmall = (HICON)LoadImageW(
            hInstance,
            MAKEINTRESOURCE(IDI_ICON1),
            IMAGE_ICON,
            GetSystemMetrics(SM_CXSMICON),
            GetSystemMetrics(SM_CYSMICON),
            LR_DEFAULTCOLOR
        );
        HICON hIconBig = (HICON)LoadImageW(
            hInstance,
            MAKEINTRESOURCE(IDI_ICON1),
            IMAGE_ICON,
            GetSystemMetrics(SM_CXICON),
            GetSystemMetrics(SM_CYICON),
            LR_DEFAULTCOLOR
        );
        if (hIconSmall)
            SendMessageW(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIconSmall);
        if (hIconBig)
            SendMessageW(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIconBig);

        EnsureDatabaseOpen(hDlg); // si existe cmod.db, rellena g_dbBuilt* 
        std::wstring t = L"cmod";
        t += DbBadge();
        SetWindowTextW(hDlg, t.c_str());

        return TRUE;
    }

    case WM_GETMINMAXINFO: {
        // lParam apunta a un MINMAXINFO
        LPMINMAXINFO pMMI = (LPMINMAXINFO)lParam;
        pMMI->ptMinTrackSize.x = 500;
        pMMI->ptMinTrackSize.y = 400;
        return TRUE;  // hemos procesado el mensaje
    }

    case WM_SIZE:
    {
        int newW = LOWORD(lParam), newH = HIWORD(lParam);
        int dx = newW - g_rcInitClient.right;
        int dy = newH - g_rcInitClient.bottom;

        // begin a batch of window-position changes
        HDWP hdwp = BeginDeferWindowPos((int)g_mapCtrls.size());

        for (auto& kv : g_mapCtrls) {
            HWND hwnd = GetDlgItem(hDlg, kv.first);
            auto& ci = kv.second;

            int x = ci.rc.left + (ci.moveX ? dx : 0);
            int y = ci.rc.top + (ci.moveY ? dy : 0);
            int w = (ci.rc.right - ci.rc.left) + (ci.sizeW ? dx : 0);
            int h = (ci.rc.bottom - ci.rc.top) + (ci.sizeH ? dy : 0);

            hdwp = DeferWindowPos(
                hdwp,
                hwnd,
                NULL,
                x, y, w, h,
                SWP_NOZORDER
            );
        }

        EndDeferWindowPos(hdwp);

        if (dx != 0) {
            HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);
            int sumInit = 0;
            for (int i = 0; i < 5; ++i) sumInit += g_colInitWidth[i];
            for (int i = 0; i < 5; ++i) {
                int colw = g_colInitWidth[i] + MulDiv(g_colInitWidth[i], dx, sumInit);
                ListView_SetColumnWidth(hList, i, colw);
            }
        }
        break;
    }
    case WM_TIMER: {
        if (wParam == IDT_SEARCH_DELAY) {
            KillTimer(hDlg, IDT_SEARCH_DELAY);
            // Lanza tu búsqueda con exact=false y randomCount=0
            DoSearch(hDlg, /*exact=*/false, /*randomCount=*/0);
            return TRUE;
        }
        break;
    }
    case WM_COMMAND: {
        if (LOWORD(wParam) == IDC_EDIT_SEARCH && HIWORD(wParam) == EN_CHANGE) {
            // Reinicia debounce-timer
            KillTimer(hDlg, IDT_SEARCH_DELAY);

            // 2) Lee el contenido actual
            wchar_t buf[256];
            GetDlgItemTextW(hDlg, IDC_EDIT_SEARCH, buf, _countof(buf));
            size_t len = wcslen(buf);

            // 3) Sólo lanzamos la búsqueda si cumple el mínimo de 3 chars
            if (len >= 3) {
                // tras DEBOUNCE_MS ms sin más cambios, disparamos DoSearch
                SetTimer(hDlg, IDT_SEARCH_DELAY, SEARCH_DELAY_MS, nullptr);
            }
            else {
                // opcionalmente limpiamos resultados o mostramos “escribe 3 chars…”
                // ListView_DeleteAllItems(GetDlgItem(hDlg, IDC_LIST_RESULTS));
                SetDlgItemTextW(hDlg, IDC_STATIC_COUNT, L"Type at least 3 letters");
            }
            return TRUE;
        }

        switch (LOWORD(wParam)) {

        case IDC_BUTTON_REPLACE_ALL:
        {
            if (g_use_pls) {
                HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);
                std::vector<PlsEntry> items;
                CollectPlsEntriesFromList(hList, /*onlySelected=*/false, items);

                std::wstring plsPathW; std::string plsUrl;
                if (!items.empty() && WriteTempPls(items, &plsPathW, &plsUrl)) {
                    std::string dde = "[open(" + plsUrl + ")]";
                    xmpfmisc->DDE(dde.c_str());
                    ClearSelectionNow();
                }
                return TRUE;
            }
            else {
                // current behavior
                xmpfmisc->DDE("key341"); // select all
                xmpfmisc->DDE("key370"); // delete selection
                // fallthrough into ADD_ALL (or duplicate the ADD_ALL logic here)
            }
        }
        case IDC_BUTTON_ADD_ALL:
        {
            if (g_use_pls) {
                HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);
                std::vector<PlsEntry> items;
                CollectPlsEntriesFromList(hList, /*onlySelected=*/false, items);

                std::wstring plsPathW; std::string plsUrl;
                if (!items.empty() && WriteTempPls(items, &plsPathW, &plsUrl)) {
                    std::string dde = "[list(" + plsUrl + ")]";
                    xmpfmisc->DDE(dde.c_str());
                    ClearSelectionNow();
                }
                return TRUE;
            }
            else {
                // your existing per-item [list(url)] loop:
                HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);
                int itemCount = ListView_GetItemCount(hList);
                for (int i = 0; i < itemCount; ++i) {
                    WCHAR wpath[MAX_PATH];
                    ListView_GetItemText(hList, i, 4, wpath, MAX_PATH);
                    std::string url = modland_url_from_pathW(wpath);
                    std::string ddeCmd = "[list(" + url + ")]";
                    xmpfmisc->DDE(ddeCmd.c_str());
                }
                return TRUE;
            }
        }

        case IDC_BUTTON_ARTIST:
        {
            DoRandomArtist(hDlg);
            return TRUE;
        }

        case IDC_BUTTON_SONGS:
        {
            HWND hCombo = GetDlgItem(hDlg, IDC_COMBO_NUMBER);
            int sel = (int)SendMessageW(hCombo, CB_GETCURSEL, 0, 0);
            if (sel != CB_ERR)
            {
                wchar_t buf[16];
                SendMessageW(hCombo, CB_GETLBTEXT, sel, (LPARAM)buf);
                buf[_countof(buf) - 1] = L'\0';  // por si acaso
                int n = _wtoi(buf);
                if (n > 0)
                    DoSearch(hDlg, /*exact=*/false, n);
            }
            return TRUE;
        }

        case IDC_BUTTON_CHECK_UPDATES:
        {
            EnableWindow(GetDlgItem(hDlg, IDC_BUTTON_CHECK_UPDATES), FALSE);

            auto* args = new CheckParams{ hDlg };
            uintptr_t th = _beginthreadex(nullptr, 0, CheckUpdatesThread, args, 0, nullptr);
            if (th) CloseHandle((HANDLE)th);
            else {
                delete args;
                EnableWindow(GetDlgItem(hDlg, IDC_BUTTON_CHECK_UPDATES), TRUE);
                MessageBoxW(hDlg, L"Could not start update check thread.", L"Error", MB_ICONERROR);
            }
            return TRUE;
        }

        case IDCANCEL:
        {
            EndDialog(hDlg, 0);
            return TRUE;
        }
        }
        break;
    }

    case WM_DB_REBUILT:
    {
        BOOL ok = (BOOL)wParam;
        xmpfmisc->ShowBubble(ok ? "DB rebuilt successfully!" : "DB rebuild failed.", 1000);
        EnableWindow(GetDlgItem(hDlg, IDC_BUTTON_CHECK_UPDATES), TRUE);
        return TRUE;
    }

    case WM_CHECK_READY:
    {
        BOOL ok = (BOOL)wParam;
        time_t remoteUtc = (time_t)lParam;

        EnableWindow(GetDlgItem(hDlg, IDC_BUTTON_CHECK_UPDATES), TRUE);

        // Compose info text
        std::wstring msg;
        if (ok) {
            msg = L"Remote allmods.zip: " + ymd_local(remoteUtc)
                + L"\nCurrent DB:       " + ymd_local(g_dbBuiltUnix)
                + ((g_dbBuiltAllFormats) ? L" (all formats)\n" : L" (default)\n");
            if (g_dbBuiltUnix && remoteUtc <= g_dbBuiltUnix) {
                msg += L"\nYour database looks up to date.\n";
            }
            else {
                msg += L"\nA newer list appears to be available.\n";
            }
            msg += L"\nChoose what to rebuild:\n"
                L"  Yes = Rebuild (default formats)\n"
                L"  No  = Rebuild (all formats)\n"
                L"  Cancel = Do nothing";
        }
        else {
            msg = L"Could not contact modland.com to read Last-Modified.\n\n"
                L"You can still rebuild:\n"
                L"  Yes = Rebuild (default formats)\n"
                L"  No  = Rebuild (all formats)\n"
                L"  Cancel = Do nothing";
        }

        int ret = MessageBoxW(
            hDlg, msg.c_str(), L"Check for updates",
            MB_ICONINFORMATION | MB_YESNOCANCEL | MB_DEFBUTTON1
        );

        if (ret == IDYES || ret == IDNO) {
            // Optional fast-exit: if we did get the server date and we're already up to date
            // for the chosen 'kind', skip the rebuild.
            bool wantAll = (ret == IDNO);
            if (ok && g_dbBuiltUnix && (remoteUtc <= g_dbBuiltUnix) && (g_dbBuiltAllFormats == wantAll)) {
                xmpfmisc->ShowBubble("Allmods is not newer — DB is up to date.", 1500);
                // If you want to refresh UI counters/etc:
                PostMessageW(hDlg, WM_DB_REBUILT, TRUE, 0);
                return TRUE;
            }

            // Launch your existing rebuild thread with the chosen mode
            EnableWindow(GetDlgItem(hDlg, IDC_BUTTON_CHECK_UPDATES), FALSE);
            auto* params = new RebuildParams{ hDlg, wantAll /*all*/ };
            uintptr_t th = _beginthreadex(nullptr, 0, RebuildThread, params, 0, nullptr);
            if (th) CloseHandle((HANDLE)th);
            else {
                delete params;
                EnableWindow(GetDlgItem(hDlg, IDC_BUTTON_CHECK_UPDATES), TRUE);
                MessageBoxW(hDlg, L"Could not start rebuild thread.", L"Error", MB_ICONERROR);
            }
        }

        return TRUE;
    }

    case WM_NOTIFY: {
        LPNMHDR pnm = (LPNMHDR)lParam;
        if (pnm->idFrom == IDC_LIST_RESULTS) {

            // --- 1) Custom-draw para sub-items sólo ---
            if (pnm->code == NM_CUSTOMDRAW) {
                LPNMLVCUSTOMDRAW cd = (LPNMLVCUSTOMDRAW)lParam;

                switch (cd->nmcd.dwDrawStage)
                {
                case CDDS_PREPAINT:
                    // Queremos notificación por ITEM
                    SetWindowLongPtr(hDlg, DWLP_MSGRESULT, CDRF_NOTIFYITEMDRAW);
                    return TRUE;

                case CDDS_ITEMPREPAINT:
                {
                    // Si la fila está seleccionada, deja el color por defecto
                    if (cd->nmcd.uItemState & CDIS_SELECTED)
                        break;

                    // Texto de la 1.ª columna (Ext)
                    wchar_t ext[16] = L"";
                    ListView_GetItemText(
                        GetDlgItem(hDlg, IDC_LIST_RESULTS),
                        (int)cd->nmcd.dwItemSpec,   // índice de fila
                        0,                          // sub-ítem 0
                        ext, _countof(ext));

                    // Decide colores
                    if (_wcsicmp(ext, L"xm") == 0) {           // azul pastel
                        cd->clrTextBk = RGB(232, 235, 255);   //  ❮  antes 200,200,255
                        cd->clrText = RGB(16, 32, 128);   //  azul marino tenue
                    }
                    else if (_wcsicmp(ext, L"it") == 0) {           // naranja vainilla
                        cd->clrTextBk = RGB(255, 248, 228);   //  ❮  antes 255,240,200
                        cd->clrText = RGB(120, 72, 0);   //  marrón medio
                    }
                    else if (_wcsicmp(ext, L"s3m") == 0) {           // verde menta
                        cd->clrTextBk = RGB(225, 255, 225);   //  ❮  antes 200,255,200
                        cd->clrText = RGB(0, 104, 0);   //  verde oscuro
                    }
                    else if (_wcsicmp(ext, L"mod") == 0) {           // rosa claro
                        cd->clrTextBk = RGB(255, 236, 236);   //  ❮  antes 255,220,220
                        cd->clrText = RGB(136, 0, 48);   //  burdeos suave
                    }
                    // CDRF_NEWFONT para aplicar colores a toda la fila
                    SetWindowLongPtr(hDlg, DWLP_MSGRESULT, CDRF_NEWFONT);
                    return TRUE;
                }
                } // switch(stage)

                // Dejar que Windows haga lo demás
                SetWindowLongPtr(hDlg, DWLP_MSGRESULT, CDRF_DODEFAULT);
                return TRUE;
            }

            // 
            if (pnm->code == LVN_COLUMNCLICK)
            {
                NMLISTVIEW* p = (NMLISTVIEW*)pnm;

                // ¿misma columna? -> alternar asc/desc.  ¿nueva? -> asc por defecto
                if (g_sortColumn == p->iSubItem)  g_sortAsc = !g_sortAsc;
                else { g_sortColumn = p->iSubItem; g_sortAsc = true; }

                // Construir datos y lanzar la ordenación
                SortData sd{ GetDlgItem(hDlg, IDC_LIST_RESULTS), g_sortColumn, g_sortAsc };

                ListView_SortItemsEx(sd.hList, ListCompare, (LPARAM)&sd);

                // (Opcional) flecha en el encabezado
                HWND hHeader = ListView_GetHeader(sd.hList);
                int colCount = Header_GetItemCount(hHeader);
                for (int i = 0; i < colCount; ++i)
                {
                    HDITEMW hd = { HDI_FORMAT };
                    Header_GetItem(hHeader, i, &hd);
                    hd.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN);          // quita flechas
                    if (i == g_sortColumn)
                        hd.fmt |= (g_sortAsc ? HDF_SORTUP : HDF_SORTDOWN);
                    Header_SetItem(hHeader, i, &hd);
                }
                return TRUE;   // ya tratado
            }

            // --- 2) Your existing double-click handler ---
            if (pnm->code == NM_DBLCLK) {
                HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);
                int sel = ListView_GetNextItem(hList, -1, LVNI_SELECTED);
                if (sel != -1) {
                    const bool alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
                    if (g_use_pls) {
                        // 1-entry PLS
                        std::vector<PlsEntry> items;
                        // ensure “selected only” collection picks that row
                        ListView_SetItemState(hList, sel, LVIS_SELECTED, LVIS_SELECTED);
                        CollectPlsEntriesFromList(hList, /*onlySelected=*/true, items);

                        std::wstring plsPathW; std::string plsUrl;
                        if (!items.empty() && WriteTempPls(items, &plsPathW, &plsUrl)) {
                            std::string dde = alt ? "[open(" + plsUrl + ")]"
                                : "[list(" + plsUrl + ")]";
                            xmpfmisc->DDE(dde.c_str());
                            ClearSelectionNow();
                        }
                    }
                    else {
                        // current behavior
                        WCHAR wbuf[MAX_PATH];
                        ListView_GetItemText(hList, sel, 4, wbuf, MAX_PATH);
                        const std::string url = modland_url_from_pathW(wbuf);
                        std::string ddeCmd = alt ? "[open(" + url + ")]"
                            : "[list(" + url + ")]";
                        xmpfmisc->DDE(ddeCmd.c_str());
                    }
                }
                return TRUE;
            }

            // --- RCLICK para popup ---
            if (pnm->code == NM_RCLICK) {
                auto plv = (NMLISTVIEW*)pnm;
                HWND hList = plv->hdr.hwndFrom;

                // 1) Hit-test para ver si el click fue sobre un ítem
                LVHITTESTINFO hti = {};
                hti.pt = plv->ptAction;  // ya está en coords cliente
                int idx = ListView_HitTest(hList, &hti);
                if (idx < 0 || !(hti.flags & LVHT_ONITEM))
                    return FALSE;

                // 2) selecciona esa fila (y conserva la selección múltiple)
                ListView_SetItemState(
                    hList,
                    idx,
                    LVIS_SELECTED | LVIS_FOCUSED,
                    LVIS_SELECTED | LVIS_FOCUSED
                );

                // 3) cuenta los seleccionados
                int selCount = ListView_GetSelectedCount(hList);

                WCHAR artistName[MAX_PATH] = L"";
                ListView_GetItemText(hList, idx, 1, artistName, _countof(artistName));

                // 4) crea el menú emergente
                HMENU hPop = CreatePopupMenu();
                AppendMenuW(hPop, MF_STRING, ID_CONTEXT_OPEN, L"Open ([ALT]-double click)");
                AppendMenuW(hPop, MF_STRING, ID_CONTEXT_ADD, L"Add to playlist (double click)");
                AppendMenuW(hPop, MF_SEPARATOR, 0, NULL);
                std::wstring msel = L"Search artist \"";
                msel += artistName;
                msel += L"\"";
                AppendMenuW(hPop, MF_STRING, ID_CONTEXT_SEARCH_ARTIST, msel.c_str());
                AppendMenuW(hPop, MF_SEPARATOR, 0, NULL);
                AppendMenuW(hPop, MF_STRING, ID_CONTEXT_COPY_URL, L"Copy song URL (modland.com) to clipboard");
                AppendMenuW(hPop, MF_SEPARATOR, 0, NULL);
                WCHAR wSong[MAX_PATH];
                ListView_GetItemText(hList, idx, 2, wSong, _countof(wSong));
                std::wstring lblSong = L"Search \"" + std::wstring(wSong) + L"\" on ModArchive (opens browser)";
                std::wstring lblArtist = L"Search \"" + std::wstring(artistName) + L"\" on ModArchive (opens browser)";
                AppendMenuW(hPop, MF_STRING, ID_CONTEXT_SEARCH_MODARCHIVE_FILE, lblSong.c_str());
                AppendMenuW(hPop, MF_STRING, ID_CONTEXT_SEARCH_MODARCHIVE_ARTIST, lblArtist.c_str());

                // 5) si hay más de uno, grayea “Open”
                if (selCount > 1) {
                    EnableMenuItem(
                        hPop,
                        ID_CONTEXT_OPEN,
                        MF_BYCOMMAND | MF_GRAYED
                    );
                }

                // 6) dispara el popup
                POINT pt = plv->ptAction;
                ClientToScreen(hList, &pt);
                SetForegroundWindow(hDlg);
                UINT cmd = TrackPopupMenu(
                    hPop,
                    TPM_RIGHTBUTTON | TPM_RETURNCMD,
                    pt.x, pt.y,
                    0,
                    hDlg,
                    NULL
                );
                DestroyMenu(hPop);

                // HWND hList = GetDlgItem(hDlg, IDC_LIST_RESULTS);
                // Para “Open” solo si hay uno seleccionado
                if (cmd == ID_CONTEXT_OPEN) {
                    if (g_use_pls) {
                        std::vector<PlsEntry> items;
                        CollectPlsEntriesFromList(hList, /*onlySelected=*/true, items);
                        std::wstring plsPathW; std::string plsUrl;
                        if (!items.empty() && WriteTempPls(items, &plsPathW, &plsUrl)) {
                            xmpfmisc->DDE(("[open(" + plsUrl + ")]").c_str());
							ClearSelectionNow();
                        }
                    }
                    else {
                        int idx = ListView_GetNextItem(hList, -1, LVNI_SELECTED);
                        if (idx != -1) {
                            WCHAR wpath[MAX_PATH]; ListView_GetItemText(hList, idx, 4, wpath, MAX_PATH);
                            const std::string url = modland_url_from_pathW(wpath);
                            xmpfmisc->DDE(("[open(" + url + ")]").c_str());
                        }
                    }
                }
                else if (cmd == ID_CONTEXT_ADD) {
                    if (g_use_pls) {
                        std::vector<PlsEntry> items;
                        CollectPlsEntriesFromList(hList, /*onlySelected=*/true, items);
                        std::wstring plsPathW; std::string plsUrl;
                        if (!items.empty() && WriteTempPls(items, &plsPathW, &plsUrl)) {
                            xmpfmisc->DDE(("[list(" + plsUrl + ")]").c_str());
							ClearSelectionNow();
                        }
                    }
                    else {
                        int idx = -1;
                        while ((idx = ListView_GetNextItem(hList, idx, LVNI_SELECTED)) != -1) {
                            WCHAR wpath[MAX_PATH]; ListView_GetItemText(hList, idx, 4, wpath, MAX_PATH);
                            const std::string url = modland_url_from_pathW(wpath);
                            xmpfmisc->DDE(("[list(" + url + ")]").c_str());
                        }
                    }
                }
                else if (cmd == ID_CONTEXT_SEARCH_ARTIST) {
                    // 1) put artistName into the edit
                    std::wstring artistNameQuoted = std::wstring(L"\"") + artistName + L"\"";
                    SetDlgItemTextW(hDlg, IDC_EDIT_SEARCH, artistNameQuoted.c_str());
                    // 2) select “Artist” in the combo
                    SendMessageW(
                        GetDlgItem(hDlg, IDC_COMBO_SEARCH),
                        CB_SETCURSEL,
                        0,  // index 0 == "Artist"
                        0
                    );
                    // 3) do an exact search
                    DoSearch(hDlg, /*exact=*/true);
                }
                else if (cmd == ID_CONTEXT_COPY_URL) {
                    // 1) pull the UTF-16 path from column 4
                    WCHAR wbuf[MAX_PATH];
                    ListView_GetItemText(hList, idx, 4, wbuf, _countof(wbuf));

                    // 2) UTF-16 → UTF-8
                    // 3) URL-escape
                    std::string url = modland_url_from_pathW(wbuf);

                    // 4) UTF-8 → UTF-16 so we can put it on the clipboard
                    std::wstring wurl = to_wide(url);

                    // 5) Copy to clipboard
                    if (OpenClipboard(hDlg)) {
                        EmptyClipboard();
                        size_t cb = (wurl.size() + 1) * sizeof(wchar_t);
                        HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, cb);
                        if (hMem) {
                            memcpy(GlobalLock(hMem), wurl.c_str(), cb);
                            GlobalUnlock(hMem);
                            SetClipboardData(CF_UNICODETEXT, hMem);
                        }
                        CloseClipboard();
                    }
                    return TRUE;
                }
                else if (cmd == ID_CONTEXT_SEARCH_MODARCHIVE_FILE)
                {
                    // get the 'song' column (sub‐item 2)
                    WCHAR wSong[MAX_PATH];
                    ListView_GetItemText(hList, idx, 2, wSong, _countof(wSong));
                    std::wstring song(wSong);
                    auto ext = PathFindExtensionW(song.c_str());
                    if (ext && *ext == L'.') {
                        song.resize(ext - song.c_str());
                    }

                    // UTF-16 → UTF-8
                    const std::string songUtf8 = to_utf8(song);

                    // URL-encode
                    std::string q = url_encode_utf8(songUtf8);

                    // build ModArchive song/file search URL
                    std::string url =
                        "https://modarchive.org/index.php?"
                        "request=search&query=" + q +
                        "&submit=Find&search_type=filename_or_songtitle";

                    // UTF-8 → UTF-16
                    std::wstring wurl = to_wide(url);

                    ShellExecuteW(NULL, L"open", wurl.c_str(), NULL, NULL, SW_SHOWNORMAL);
                    return TRUE;
                }
                else if (cmd == ID_CONTEXT_SEARCH_MODARCHIVE_ARTIST) {
                    // get the 'artist' column (sub‐item 1)
                    WCHAR wArtist[MAX_PATH];
                    ListView_GetItemText(hList, idx, 1, wArtist, _countof(wArtist));

                    // UTF-16 → UTF-8
                    // URL-encode
                    const std::string q = url_encode_utf8(to_utf8(std::wstring(wArtist)));

                    // build ModArchive artist search URL
                    std::string url =
                        "https://modarchive.org/index.php?"
                        "query=" + q +
                        "&submit=Find&request=search&search_type=guessed_artist";

                    // UTF-8 → UTF-16
                    std::wstring wurl = to_wide(url);

                    ShellExecuteW(NULL, L"open", wurl.c_str(), NULL, NULL, SW_SHOWNORMAL);
                    return TRUE;
                }
                return TRUE;
            }
        }
        break;
    }

    }
    return FALSE;
}

static void WINAPI OpenSearchShortcut(void) {
    // std::string cmdOpen = "[open(file:///G:/radix.xm)]";
    // std::string cmdOpen = "[open(G:\\modules\\modarchive\\radix\\radix_[68796]_colours 2.xm)]";
    /*
    std::string cmdOpen = "[open(https://modland.com/pub/modules/Impulsetracker/Arachno/through the night.it)]";
    xmpfmisc->DDE(cmdOpen.c_str());
    cmdOpen = "[list(https://modland.com/pub/modules/Impulsetracker/Chonty/perfect day.it)]";
    xmpfmisc->DDE(cmdOpen.c_str());
    */

    // xmpfmisc->DDE("key2");

    if (!hSearchDlg) {
        INITCOMMONCONTROLSEX icex = { sizeof(icex), ICC_LISTVIEW_CLASSES };
        InitCommonControlsEx(&icex);

        HWND hMain = (HWND)xmpfmisc->GetWindow();
        hSearchDlg = CreateDialogParam(
            hInstance,
            MAKEINTRESOURCE(IDD_DIALOG1),
            hMain,
            SearchDlgProc,
            0
        );
    }
    if (hSearchDlg) {
        ShowWindow(hSearchDlg, SW_SHOWNORMAL);
        SetForegroundWindow(hSearchDlg);
        HWND hEdit = GetDlgItem(hSearchDlg, IDC_EDIT_SEARCH);
        SetFocus(hEdit);
    }
}
// Entrada del plugin
// extern "C" __declspec(dllexport)
XMPDSP *WINAPI XMPDSP_GetInterface2(DWORD face, InterfaceProc faceproc) {
    if (face != XMPDSP_FACE) return nullptr;
    g_faceproc = faceproc;

    static XMPDSP plugin = {
        XMPDSP_FLAG_NODSP,    // general plugin
        "cmod",               // name
        Plugin_About,         // About
        Plugin_Init,          // New/init
        Plugin_Exit,          // Free/exit
        Plugin_GetDescription,// GetDescription
        MyConfig,             // Config (diálogo)
        Plugin_GetConfig,     // GetConfig
        Plugin_SetConfig,     // SetConfig
        nullptr,              // NewTrack
        nullptr,              // SetFormat
        nullptr,              // Reset
        nullptr,              // Process
        nullptr               // NewTitle
    };

    // Crear atajo
    xmpfmisc = (XMPFUNC_MISC*)faceproc(XMPFUNC_MISC_FACE);
    xmpffile = (XMPFUNC_FILE*)faceproc(XMPFUNC_FILE_FACE);
    static const XMPSHORTCUT shortcut = {
        0x20001,
        "cmod - Open search dialog",
        OpenSearchShortcut
    };
    xmpfmisc->RegisterShortcut(&shortcut);
    
    LoadConfigIni();

    std::srand((unsigned)std::time(nullptr));

    return &plugin;
}

BOOL WINAPI DllMain(HINSTANCE hDLL, DWORD reason, LPVOID) {
    if (reason == DLL_PROCESS_ATTACH) {
        hInstance = hDLL;
        DisableThreadLibraryCalls(hDLL);
    }
    return TRUE;
}

