// editor_platform.cpp — the ReaSamplerEditor's IPlugView + Win32 window plumbing:
// platform-type/resize negotiation, the child window class + creation/destruction, the
// sync timer lifetime, the WM_* dispatch (wndProc — paint, mouse, keyboard, capture-loss
// rollback, drop-accept, timer), and the non-Windows stubs (Windows is the only build
// target; the TU still compiles elsewhere).

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32
#include <windowsx.h>  // GET_X_LPARAM / GET_Y_LPARAM
#include <shellapi.h>  // DragAcceptFiles / DragQueryFile / DragFinish — drop-accept
#endif

#include "shell/instrument/editor_internal.h"  // (transitively: lice + the kit, Windows only)
#include "shell/instrument/reasampler_processor.h"

using namespace Steinberg;

namespace reasampler::vst {

#ifdef _WIN32
namespace {
constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor";

// The change-detection poll (WM_TIMER on the child window). A low-frequency UI-thread
// timer: responsive enough that a recapture/ingest/assign refreshes within a bounded
// cadence, yet cheap — three small ext-state reads per tick, coalescing many bumps
// between ticks into one reload. 500 ms is a deliberate build-time residual. The id is a
// per-window SetTimer id (any nonzero).
constexpr UINT_PTR kSyncTimerId = 1;
constexpr UINT     kSyncTimerIntervalMs = 500;
}  // namespace
#endif

tresult PLUGIN_API ReaSamplerEditor::isPlatformTypeSupported(FIDString type) {
#ifdef _WIN32
    if (type && std::string(type) == kPlatformTypeHWND) return kResultTrue;
#endif
    return kResultFalse;
}

tresult PLUGIN_API ReaSamplerEditor::canResize() {
    return kResultTrue;
}

tresult PLUGIN_API ReaSamplerEditor::checkSizeConstraint(ViewRect* rect) {
    // Enforce a minimum usable floor: at least 560 wide and 460 tall. The host calls this
    // before every resize; clamp the proposed rect in place and return kResultTrue so the host
    // applies the (possibly adjusted) rect rather than the raw user drag. 560x460 keeps the
    // Sample face's title + hero waveform + cluster + a few control rows visible (the control
    // strip clips gracefully below the panel bottom); anything smaller would clip essential UI.
    // The default 840x620 is above this floor.
    constexpr int kMinW = 560;
    constexpr int kMinH = 460;
    if (!rect) return kResultFalse;
    if (rect->getWidth() < kMinW) rect->right = rect->left + kMinW;
    if (rect->getHeight() < kMinH) rect->bottom = rect->top + kMinH;
    return kResultTrue;
}

#ifdef _WIN32

void ReaSamplerEditor::invalidate() {
    if (childHwnd_) InvalidateRect(childHwnd_, nullptr, FALSE);
}

void ReaSamplerEditor::attachedToParent() {
    HWND parent = static_cast<HWND>(systemWindow);
    if (!parent) return;

    HINSTANCE hInst =
        reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE));
    if (!hInst) hInst = GetModuleHandle(nullptr);

    static bool classRegistered = false;
    if (!classRegistered) {
        WNDCLASSW wc{};
        wc.lpfnWndProc = &ReaSamplerEditor::wndProc;
        wc.hInstance = hInst;
        wc.lpszClassName = kChildClassName;
        wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
        wc.style = CS_HREDRAW | CS_VREDRAW;
        RegisterClassW(&wc);
        classRegistered = true;
    }

    // Create the kit's cached AA fonts before the first paint. Idempotent, so a reopen (or a
    // co-resident embed strip that also inits) is a cheap no-op. Not torn down on editor close:
    // the embed strip in the same binary shares the kit's process-global font set, so a
    // per-view shutdown could free fonts still in use by the other view. The tiny static HFONT
    // set is reclaimed by the OS at module unload.
    kitFontsInit();

    refreshFromBank();

    const ViewRect& r = getRect();
    childHwnd_ = CreateWindowExW(0, kChildClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0,
                                 r.getWidth(), r.getHeight(), parent, nullptr, hInst, nullptr);
    if (childHwnd_) {
        SetWindowLongPtr(childHwnd_, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
        // Accept OS file drops on the editor window (WM_DROPFILES). The drop is not ingested
        // here (the relay is degraded — see onFilesDropped); accepting it lets us show the
        // "drop on the panel" affordance instead of the OS bouncing the drop silently.
        DragAcceptFiles(childHwnd_, TRUE);
        // Start the change-detection poll (UI thread). Tied to the child window's lifetime —
        // created here, killed in removedFromParent — so an instance whose editor is closed
        // does not poll.
        SetTimer(childHwnd_, kSyncTimerId, kSyncTimerIntervalMs, nullptr);
        // Poll once immediately so a pending assignment (an ingest fired while this editor was
        // closed) or a bank change applies the instant the editor opens, rather than waiting up
        // to one timer interval. refreshFromBank above already primed the view; this folds in
        // any pending assign/generation so the just-opened editor shows the assigned capture.
        onSyncTimer();
    }
}

void ReaSamplerEditor::removedFromParent() {
    if (childHwnd_) {
        KillTimer(childHwnd_, kSyncTimerId);  // stop the poll before the window goes away
        DestroyWindow(childHwnd_);
        childHwnd_ = nullptr;
    }
}

tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) {
    tresult res = CPluginView::onSize(newSize);
    if (childHwnd_ && newSize) {
        MoveWindow(childHwnd_, 0, 0, newSize->getWidth(), newSize->getHeight(), TRUE);
        thumbCache_.clear();  // thumbnails are width-bound; a resize invalidates them
    }
    return res;
}

LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
                                           LPARAM lParam) {
    auto* self =
        reinterpret_cast<ReaSamplerEditor*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
    switch (msg) {
        case WM_PAINT: {
            PAINTSTRUCT ps{};
            HDC hdc = BeginPaint(hwnd, &ps);
            if (self) self->paint(hdc);
            EndPaint(hwnd, &ps);
            return 0;
        }
        case WM_LBUTTONDOWN:
            if (self) {
                SetCapture(hwnd);  // keep receiving moves/up if the cursor leaves the child
                SetFocus(hwnd);    // take keyboard focus so WM_CHAR reaches the search box
                self->onMouseDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
            }
            return 0;
        case WM_MOUSEMOVE:
            if (self) {
                const int mx = GET_X_LPARAM(lParam);
                const int my = GET_Y_LPARAM(lParam);
                // Hover feedback: resolve the element under the pointer and repaint on change.
                // Arm WM_MOUSELEAVE once per "over" cycle so the hover clears when the pointer
                // leaves the child (TrackMouseEvent is one-shot).
                if (!self->mouseTracking_) {
                    TRACKMOUSEEVENT tme{};
                    tme.cbSize = sizeof(tme);
                    tme.dwFlags = TME_LEAVE;
                    tme.hwndTrack = hwnd;
                    TrackMouseEvent(&tme);
                    self->mouseTracking_ = true;
                }
                // While a drag is in flight the drag owns the surface; skip hover resolution
                // (a hover repaint mid-drag would fight the live drag feedback).
                if (self->drag_ == DragKind::kNone) self->resolveHover(mx, my);
                self->onMouseMove(mx, my);
            }
            return 0;
        case WM_MOUSELEAVE:
            if (self) {
                self->mouseTracking_ = false;
                if (self->hover_.kind != HoverKind::kNone) {
                    self->hover_ = HoverTarget{};
                    self->invalidate();
                }
            }
            return 0;
        case WM_MOUSEWHEEL:
            // Browser scroll. GET_WHEEL_DELTA_WPARAM is signed; positive == wheel up.
            if (self) self->onMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam));
            return 0;
        case WM_CHAR:
            // Type-to-filter search keystrokes (only acted on when the search box is focused).
            if (self) self->onSearchChar(static_cast<unsigned int>(wParam));
            return 0;
        case WM_GETDLGCODE:
            // Claim all keys (incl. chars) so the host doesn't eat them before WM_CHAR (search).
            return DLGC_WANTCHARS | DLGC_WANTARROWS;
        case WM_LBUTTONUP:
            if (self) {
                self->onMouseUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
                ReleaseCapture();
            }
            return 0;
        case WM_RBUTTONDOWN:
            // Right-click — the curve popup's primary node-delete affordance. Routed
            // explicitly (the child wndproc historically handled only left-button).
            if (self) self->onMouseRDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
            return 0;
        case WM_RBUTTONUP:
            return 0;  // claimed so the pair never reaches DefWindowProc (no context menu)
        case WM_CAPTURECHANGED:
            // Capture stolen mid-drag (modal dialog, alt-tab, etc.) — restore map_ to its
            // pre-grab snapshot so the in-flight live-drag mutation is rolled back, then reset
            // the drag state machine so stale capture-less WM_MOUSEMOVEs don't keep editing.
            // Mirror of the panel shell's WM_CAPTURECHANGED handler (panel_window.cpp).
            if (self) {
                // A held preview note must be released here too (peer of WM_LBUTTONUP) — capture
                // loss otherwise leaves the momentary-key voice hung with no note-off.
                if (self->previewingNote_ >= 0) {
                    if (self->processor_) self->processor_->previewNoteOff(self->previewingNote_);
                    self->previewingNote_ = -1;
                    self->invalidate();
                }
                if (self->drag_ != DragKind::kNone) {
                    // A scrollbar drag + the processor-side deck knobs (preview velocity -2 /
                    // voice count / master gain) are transient (they mutate no parameter, so
                    // dragStartParams_ is not a rollback target) — reset drag state only.
                    // Every parameter-editing drag rolls its live mutation back to the snapshot.
                    const bool transient = self->drag_ == DragKind::kScrollThumb ||
                        (self->drag_ == DragKind::kDeckKnob &&
                         (self->dragParamId_ == -2 ||
                          self->dragParamId_ == static_cast<int>(ParamControl::kVoiceCount) ||
                          self->dragParamId_ == static_cast<int>(ParamControl::kMasterGain)));
                    if (!transient) self->params_ = self->dragStartParams_;
                    self->drag_ = DragKind::kNone;
                    self->dragParamId_ = -1;
                    self->curvePointIndex_ = -1;  // curve-node drag state (peer reset)
                    // No cursor position is available here to re-resolve hover (unlike
                    // onMouseUp's release coordinates), so clear rather than leave it naming
                    // wherever the drag started.
                    self->hover_ = HoverTarget{};
                    self->invalidate();
                }
            }
            return 0;
        case WM_DROPFILES: {
            // Count the dropped files and flash the affordance. We do not read/ingest the paths
            // (the instrument never ingests — the relay to the extension is unshipped);
            // DragQueryFile with 0xFFFFFFFF just returns the count for the banner.
            HDROP drop = reinterpret_cast<HDROP>(wParam);
            const UINT count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0);
            DragFinish(drop);
            if (self) self->onFilesDropped(static_cast<int>(count));
            return 0;
        }
        case WM_TIMER:
            if (self && wParam == kSyncTimerId) self->onSyncTimer();
            return 0;
        case WM_ERASEBKGND:
            return 1;  // fully repaint in WM_PAINT; skip the flicker-inducing erase
        default:
            return DefWindowProcW(hwnd, msg, wParam, lParam);
    }
}

#else  // non-Windows: not a build target, but keep the TU compilable.

void ReaSamplerEditor::attachedToParent() {}
void ReaSamplerEditor::removedFromParent() {}
tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) {
    return CPluginView::onSize(newSize);
}

#endif  // _WIN32

}  // namespace reasampler::vst
