// panel_window.cpp — window-lifecycle seam of the docked bank panel. Owns the SWELL
// dialog (docked via DockWindowAddEx / undocked via DockWindowRemove), the dialog
// proc routing to the input/drag/render/audition seams, the OS drop-target opt-in
// (WM_DROPFILES -> ingest), and the shared PanelState blob's definition. The panel
// never inserts into the arrange. Dock title + persisted-position identstr both
// come from app_version (channel-qualified).
//
// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested.

#include <string>
#include <vector>

#include "shell/panel/panel_state.h"
#include "shell/panel/panel_window.h"

#include "shell/panel/draw_kit.h"
#include "shell/actions/ingest.h"
#include "shell/actions/package_import_action.h"
#include "core/package/import_plan.h"
#include "core/version/app_version.h"
#include "shell/persist/session.h"  // ReaSamplerSession::ledgerStatus() — panel_state.h only forward-declares it

#ifdef _WIN32
#include <windowsx.h>  // GET_X_LPARAM / GET_Y_LPARAM (SWELL supplies them on mac/linux)
#include <shellapi.h>  // DragAcceptFiles / DragQueryFile / DragFinish
#endif

#include "resource.h"

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_DockWindowAddEx
#define REAPERAPI_WANT_DockWindowActivate
#define REAPERAPI_WANT_DockWindowRemove
#define REAPERAPI_WANT_GetMainHwnd
#define REAPERAPI_WANT_ShowConsoleMsg
#include "reaper_plugin_functions.h"

// main.cpp owns the module instance handle.
extern REAPER_PLUGIN_HINSTANCE g_hInst;

namespace reasampler::panel {

// Defined here — the lifecycle seam owns the state's lifetime.
PanelState g_panel;

namespace {

bool isPackagePath(const std::string& path) {
    static const std::string kExt = ".rsbank";
    if (path.size() <= kExt.size()) return false;
    std::string tail = path.substr(path.size() - kExt.size());
    for (char& c : tail)
        if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
    return tail == kExt;
}

// DragQueryFile(hDrop, 0xFFFFFFFF, ...) returns the file count; each path is then
// queried by index (length first, excludes NUL, then a sized buffer). DragFinish
// always frees the shell-allocated drop buffer. A .rsbank is a whole bank, not audio,
// so it routes to the import verb (one new bank each); everything else keeps the
// existing ingest route — multi-file drop imports all into the active bank (bank-fill
// only, no assignment to any live instance).
void handleDropFiles(HDROP hDrop) {
    std::vector<std::string> paths;
    std::vector<std::string> packages;
    const UINT count = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
    paths.reserve(count);
    for (UINT i = 0; i < count; ++i) {
        const UINT len = DragQueryFile(hDrop, i, nullptr, 0);
        if (len == 0) continue;
        std::vector<char> buf(static_cast<std::size_t>(len) + 1, '\0');
        DragQueryFile(hDrop, i, buf.data(), static_cast<UINT>(buf.size()));
        std::string p(buf.data());
        if (p.empty()) continue;
        if (isPackagePath(p)) packages.push_back(std::move(p));
        else paths.push_back(std::move(p));
    }
    DragFinish(hDrop);
    if (g_panel.session && !packages.empty()) {
        // One refusal block for the whole drop, not one per dropped .rsbank: the gate
        // decision is the same for all N (session state does not change mid-drop), so
        // checking it here first avoids doImportBankPackageFile's own per-file gate
        // check printing the identical console block N times.
        const package::LedgerRefusal refusal =
            package::importLedgerRefusal(g_panel.session->ledgerStatus());
        if (refusal != package::LedgerRefusal::None) {
            ShowConsoleMsg(
                package::ledgerRefusalMessage(refusal, version::extStateNamespace()).c_str());
        } else {
            for (const std::string& pkg : packages)
                doImportBankPackageFile(*g_panel.session, pkg);
        }
    }
    if (!paths.empty()) ingestDroppedFiles(paths);
}

WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
        case WM_DROPFILES:
            handleDropFiles(reinterpret_cast<HDROP>(wParam));
            return 0;
        case WM_PAINT: {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            paintPanel(hwnd, hdc);
            EndPaint(hwnd, &ps);
            return 0;
        }
        case WM_LBUTTONDOWN: {
            SetFocus(hwnd);
            handleClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
            return 0;
        }
        case WM_MOUSEMOVE:
            onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
            return 0;
        case WM_LBUTTONUP:
            onLBtnUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
            return 0;
        case WM_RBUTTONDOWN:
            SetFocus(hwnd);
            handleRightClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
            return 0;
        case WM_CAPTURECHANGED:
            // Capture lost (pointer left pre-threshold, or another window stole it
            // mid-drag) — cancel the drag as a no-op, mirroring onLBtnUp's reset.
            if (g_panel.dragArmed || g_panel.dragging) {
                resetDragState();
                SetCursor(LoadCursor(nullptr, IDC_ARROW));
                invalidatePanel();
            }
            return 0;
        case WM_MOUSEWHEEL: {
            // Unlike button messages, WM_MOUSEWHEEL carries SCREEN coords in lParam
            // (Win32 and SWELL agree), so convert to client space first. Wheel delta
            // is the HIWORD of wParam. Consume (return 1) only when the footer acts.
            POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
            ScreenToClient(hwnd, &pt);
            const int delta = static_cast<short>(HIWORD(wParam));
            return handleWheel(pt.x, pt.y, delta) ? 1 : 0;
        }
        case WM_DESTROY:
            if (GetCapture() == hwnd) ReleaseCapture();
            stopAudition();
            g_panel.selection = Selection{};
            resetDragState();
            g_panel.hovered = Hover{};
            g_panel.tooltipShown = false;
            g_panel.hwnd = nullptr;
            g_panel.open = false;
            return 0;
        default:
            break;
    }
    return 0;
}

void openPanel() {
    if (g_panel.open && g_panel.hwnd) {
        DockWindowActivate(g_panel.hwnd);
        return;
    }
    initPreview();

    // Idempotent — a reopen after closePanel (fonts left alive) is a cheap no-op;
    // torn down once at bankPanelShutdown.
    kitFontsInit();

    g_panel.hwnd = CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL),
                                     GetMainHwnd(), dlgProc, 0);
    if (!g_panel.hwnd) return;

    // identstr is a REAPER-global collision surface keying the persisted dock
    // position — channel-qualified so beta doesn't fight over stable's slot.
    DockWindowAddEx(g_panel.hwnd, dockTitle().c_str(), dockIdent().c_str(), true);
    DockWindowActivate(g_panel.hwnd);
    g_panel.open = true;

    // DragAcceptFiles is native Win32 (shellapi.h); SWELL doesn't expose it, so the
    // accept opt-in is Windows-only. DragQueryFile/DragFinish ARE SWELL-provided,
    // so a drop delivered by other means would still ingest.
#ifdef _WIN32
    DragAcceptFiles(g_panel.hwnd, TRUE);
#endif

    registerAccel();

    reconcileShownBank();
    refreshFingerprint();
}

void closePanel() {
    if (GetCapture() == g_panel.hwnd) ReleaseCapture();
    stopAudition();
    g_panel.selection = Selection{};
    resetDragState();
    unregisterAccel();
    if (g_panel.hwnd) {
        DockWindowRemove(g_panel.hwnd);
        DestroyWindow(g_panel.hwnd);
        g_panel.hwnd = nullptr;
    }
    g_panel.open = false;
}

}  // namespace

}  // namespace reasampler::panel

namespace reasampler {

void bankPanelInit(ReaSamplerSession* session) {
    panel::g_panel.session = session;
}

// Alt+D hides the docker without destroying the window, leaving HWND/g_panel.open
// live but IsWindowVisible false — the live query is the source of truth for
// toggle decisions and the Actions-list checkmark.
static bool panelEffectivelyVisible() {
    return panel::g_panel.hwnd && IsWindowVisible(panel::g_panel.hwnd);
}

void bankPanelToggle() {
    if (panelEffectivelyVisible())
        panel::closePanel();
    else
        panel::openPanel();
}

bool bankPanelIsOpen() {
    return panelEffectivelyVisible();
}

void bankPanelInvalidate() {
    if (panel::g_panel.hwnd) InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE);
}

void bankPanelShutdown() {
    panel::closePanel();
    panel::deinitPreview();
    kitFontsShutdown();
    panel::g_panel.cache.clear();
    panel::g_panel.session = nullptr;
}

}  // namespace reasampler
