// editor_paint_curve.cpp — the velocity-curve surfaces: the VELOCITY deck group's mini
// thumbnails and the modal popup sheet that hosts the full editor. Band-independent (the
// popup floats over the whole face). Windows-only.

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32

#include <vector>

#include "core/instrument/ui/curve_popup.h"     // centered curve-popup sheet geometry
#include "shell/instrument/editor_internal.h"   // kit adapters + curveBoxFromRect
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using namespace reasampler::ui;              // kit vocabulary
using namespace reasampler::instrument::ui;  // popup geometry

namespace {

// Both at the stroker's opaque-core floor (core/ui/CLAUDE.md) — below 2 px, peak alpha
// modulates with pixel-grid alignment instead of pinning solid.
constexpr float kMiniTracePx = 2.0f;
constexpr float kCurveTracePx = 2.0f;

const char* curveTitle(CurveTarget target) {
    switch (target) {
        case CurveTarget::kPitch:  return "VELOCITY -> PITCH";
        case CurveTarget::kFilter: return "VELOCITY -> FILTER";
        case CurveTarget::kAmp:
        case CurveTarget::kNone:
            return "VELOCITY -> AMP";
    }
    return "VELOCITY -> AMP";
}

}  // namespace

void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, CurveTarget target,
                                        bool disabled, bool hovered) {
    if (r.width <= 0 || r.height <= 0) return;
    // A hairline-bordered bg/cell square with the live velocity curve traced in miniature
    // (no node markers at this scale). Hover lifts it; it draws Active (accent-primary
    // border) while its popup is open, and re-renders live as the popup edits the curve.
    fillSurface(bmp, toKitBox(r), Role::BgCell,
                disabled ? InteractionState::Disabled
                         : (hovered ? InteractionState::Hover : InteractionState::Rest));
    const KitColor border = (!disabled && curvePopup_ == target)
                                ? roleColor(Role::AccentPrimary)
                                : roleColor(Role::LineHairline);
    LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(border), 1.0f, 0);
    const VelocityCurve& curve = curveFor(target);
    const int inset = 3;
    const VelocityCurve::Box mini{r.x + inset, r.y + inset, r.width - 2 * inset,
                                  r.height - 2 * inset};
    if (mini.width > 1 && mini.height > 1) {
        // A bipolar thumbnail gets its zero line, without which a flat-at-zero curve and a
        // flat-at-minimum one would draw identically at this scale.
        if (curve.domain() == instrument::engine::CurveDomain::Bipolar) {
            const int zy = curve.pixelFromPoint(mini, {0.0, 0.0}).y;
            LICE_Line(bmp, mini.left, zy, mini.left + mini.width, zy,
                      toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);
        }
        const LICE_pixel trace =
            toLice(roleColor(disabled ? Role::LineHairline : Role::AccentSecondary));
        std::vector<ui::StrokePoint>& pts = scratchPoints();
        pts.clear();
        for (int px = 0; px <= mini.width; ++px) {
            const int mx = mini.left + px;
            const double vel = curve.pointFromPixel(mini, mx, mini.top).velocity;
            const auto p = curve.subpixelFromPoint(mini, {vel, curve.eval(vel)});
            pts.push_back(ui::StrokePoint{static_cast<float>(mx), static_cast<float>(p.y)});
        }
        strokePolylineAA(bmp, pts, kMiniTracePx, trace);
    }
}

void ReaSamplerEditor::paintCurvePopup(LICE_IBitmap* bmp, int w, int h) {
    // The 0.50-alpha bg/base wash (lighter than Browse's 0.82 — a focused sub-editor; the
    // face stays legible behind it), then the centered sheet.
    LICE_FillRect(bmp, 0, 0, w, h, toLice(roleColor(Role::BgBase)), 0.50f, 0);
    const CurvePopupLayout pl = computeCurvePopup(w, h);
    fillSurface(bmp, toKitBox(pl.sheet), Role::BgPanel, InteractionState::Rest);
    LICE_DrawRect(bmp, pl.sheet.x, pl.sheet.y, pl.sheet.width - 1,
                  pl.sheet.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0);
    kitText(bmp, pl.title, curveTitle(curvePopup_), Font::Micro, Role::TextDim);
    {
        const KitButtonBox box{toKitBox(pl.close)};
        const InteractionState st = isHovered(HoverKind::kPopupClose, -1)
                                        ? InteractionState::Hover
                                        : InteractionState::Rest;
        drawButton(bmp, box, "x", st, /*warn=*/false);
    }
    // The full-size editor: one draw path + the one curveBoxFromRect mapping formula, so
    // trace/handles/drag-off cues cannot drift from the hit-test.
    paintVelocityCurve(bmp, pl.curveBox);
}

void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r) {
    if (r.width <= 0 || r.height <= 0) return;  // defensive (degenerate rect)

    // The bordered box: a panel surface + hairline border, drawn by palette role. No corner
    // caption — the popup sheet's own title labels this context (the popup is the only host).
    fillSurface(bmp, toKitBox(r), Role::BgPanel, InteractionState::Rest);
    LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1,
                  toLice(roleColor(Role::LineHairline)), 1.0f, 0);

    const VelocityCurve::Box box = curveBoxFromRect(r);
    if (box.width <= 0 || box.height <= 1) return;
    const VelocityCurve& curve = editedCurve();

    // A bipolar editor needs its zero axis drawn: it is the whole "off" reading, and without
    // it the default flat curve is indistinguishable from any other flat one.
    if (curve.domain() == instrument::engine::CurveDomain::Bipolar) {
        const int zy = curve.pixelFromPoint(box, {0.0, 0.0}).y;
        LICE_Line(bmp, box.left, zy, box.left + box.width, zy,
                  toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);
    }

    // Trace the monotone spline — ONE eval per x column over the mapping box, in the categorical
    // secondary accent (the same grammar as the envelope trace over the waveform). The x ->
    // velocity and value -> y mappings both go through the pure module so the trace, the node
    // handles, and the hit-test all share one coordinate system (sub-pixel for the trace — see
    // subpixelFromPoint).
    const LICE_pixel line = toLice(roleColor(Role::AccentSecondary));
    std::vector<ui::StrokePoint>& pts = scratchPoints();
    pts.clear();
    for (int px = 0; px <= box.width; ++px) {
        const int cx = box.left + px;
        const double vel = curve.pointFromPixel(box, cx, box.top).velocity;
        const auto p = curve.subpixelFromPoint(box, {vel, curve.eval(vel)});
        pts.push_back(ui::StrokePoint{static_cast<float>(cx), static_cast<float>(p.y)});
    }
    strokePolylineAA(bmp, pts, kCurveTracePx, line);

    // Draggable node handles (mirror of the envelope overlay's): accent-primary squares lifted
    // to accent-hot when grabbed or hovered, or warn when a drag-off delete is armed (cursor
    // has passed kCurveDragOffMargin outside the box — release will delete the node).
    const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary));
    const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot));
    const LICE_pixel handleWarn = toLice(roleColor(Role::Warn));
    // Drag-off check: during a kCurveNode drag on THIS box, is the live cursor beyond the margin?
    const bool dragOffArmed = (drag_ == DragKind::kCurveNode && dragCurveRect_.x == r.x &&
                               dragCurveRect_.y == r.y) &&
                              (dragCurX_ < r.x - kCurveDragOffMargin ||
                               dragCurX_ > r.right() + kCurveDragOffMargin ||
                               dragCurY_ < r.y - kCurveDragOffMargin ||
                               dragCurY_ > r.bottom() + kCurveDragOffMargin);
    for (std::size_t i = 0; i < curve.points().size(); ++i) {
        const auto np = curve.pixelFromPoint(box, curve.points()[i]);
        const bool grabbed = (drag_ == DragKind::kCurveNode &&
                              curvePointIndex_ == static_cast<int>(i));
        const bool hot = grabbed || isHovered(HoverKind::kCurveNode, static_cast<int>(i));
        // A grabbed node in drag-off territory draws warn to signal "release will delete."
        const LICE_pixel col = (grabbed && dragOffArmed) ? handleWarn
                                                         : (hot ? handleHot : handle);
        const int nr = 3;
        LICE_FillRect(bmp, np.x - nr, np.y - nr, 2 * nr, 2 * nr, col, 1.0f, 0);
    }
}

}  // namespace reasampler::vst

#endif  // _WIN32
