// editor_paint_chrome.cpp — the CHROME band's painter: the toolbar row (product title +
// live readout, then the control run — preview, preview-velocity knob, Loop Off|On,
// Mono|Stereo, Browse) over the strip row, which the piano strip has to itself. Windows-only;
// all rects come from the pure sample_chrome interior and the pure keyboard_strip geometry.

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32

#include <cstdio>
#include <string>

#include "core/instrument/note/musical_division.h"  // divisionLabel (the Hold readout)
#include "core/instrument/ui/keyboard_strip.h"  // StripLayout / keyRect / noteName
#include "core/instrument/ui/knob_deck.h"       // kDeckKnobSize (the shared knob square)
#include "core/ui/tooltip.h"                    // computeTooltip (shared placement math)
#include "core/version/app_version.h"           // vstPluginName (channel-derived title band)
#include "shell/instrument/editor_internal.h"   // kit adapters + knob face
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using namespace reasampler::ui;              // kit vocabulary
using namespace reasampler::instrument::ui;  // chrome geometry + keyboard strip
using namespace reasampler::instrument::map; // SampleRefs / findRef (title readout fallback)

namespace {

// Every text element on the toolbar row draws at this one size/weight — including the
// product title, which used to be the row's odd one out.
constexpr Font kToolbarFont = Font::Label;

// Kit font is proportional, so the char width is a generous estimate (pads, never clips).
constexpr int kTooltipCharPx = 7;
constexpr int kTooltipTextH = 14;

constexpr int kRootBadgeW = 38;
constexpr int kRootBadgeH = 13;

// The piano strip: white keys tiled at one width, black keys overlaid at one width, each
// tinted with its spectral hue so pitch position reads at a glance. `hoverNote` is outlined
// (-1 for none). All rects are strip-local; `area` supplies the origin.
void drawKeyboard(LICE_IBitmap* bmp, const Rect& area, const StripLayout& sl, int hoverNote) {
    fillSurface(bmp, toKitBox(area), Role::BgBase, InteractionState::Rest);
    if (sl.keys.empty()) return;

    const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
    const LICE_pixel shadow = toLice(roleColor(Role::BgBase));
    const auto hueOf = [](int n) {
        return toLice(spectralColor(static_cast<double>(n) / (kStripKeyCount - 1)));
    };

    for (int n = 0; n < kStripKeyCount; ++n) {
        if (!isNaturalKey(n)) continue;
        const Rect k = keyRect(sl, n);
        LICE_FillRect(bmp, area.x + k.x, area.y + k.y, k.width, k.height, hueOf(n), 0.55f, 0);
        LICE_Line(bmp, area.x + k.right() - 1, area.y + k.y, area.x + k.right() - 1,
                  area.y + k.bottom() - 1, hairline, 0.6f, 0, false);
    }
    // Blacks last: they overlap the whites they straddle.
    for (int n = 0; n < kStripKeyCount; ++n) {
        if (isNaturalKey(n)) continue;
        const Rect k = keyRect(sl, n);
        LICE_FillRect(bmp, area.x + k.x, area.y + k.y, k.width, k.height, shadow, 1.0f, 0);
        LICE_FillRect(bmp, area.x + k.x, area.y + k.y, k.width, k.height, hueOf(n), 0.35f, 0);
    }
    if (hoverNote >= 0) {
        const Rect k = keyRect(sl, hoverNote);
        LICE_DrawRect(bmp, area.x + k.x, area.y + k.y, k.width - 1, k.height - 1,
                      toLice(roleColor(Role::TextPrimary)), 0.8f, 0);
    }
}

// The root affordance: the root key lit accent-primary with a static glow, plus a name badge
// (a key is far too narrow to carry text itself). The badge is clamped inside the strip.
void drawRootKey(LICE_IBitmap* bmp, const Rect& area, const StripLayout& sl, int root) {
    if (sl.keys.empty()) return;
    const Rect k = rootMarkerRect(sl, root);
    const int kx = area.x + k.x;
    const LICE_pixel accent = toLice(roleColor(Role::AccentPrimary));
    const LICE_pixel glow = toLice(roleColor(Role::AccentHot));
    // Static glow: a wider low-alpha halo behind the lit key (a drawn state, not a pulse).
    LICE_FillRect(bmp, kx - 3, area.y + k.y, k.width + 6, k.height, glow, 0.30f, 0);
    LICE_FillRect(bmp, kx, area.y + k.y, k.width, k.height, accent, 1.0f, 0);

    const int badgeH = (std::min)(kRootBadgeH, area.height);
    const int badgeW = (std::min)(kRootBadgeW, area.width);
    int bx = kx + (k.width - badgeW) / 2;
    bx = (std::max)(area.x, (std::min)(bx, area.right() - badgeW));
    const Rect badge = Rect::ltrb(bx, area.bottom() - badgeH, bx + badgeW, area.bottom());
    LICE_FillRect(bmp, badge.x, badge.y, badge.width, badge.height, accent, 0.92f, 0);
    kitTextCentered(bmp, badge, noteName(root).c_str(), Font::Micro, Role::BgBase);
}

}  // namespace

void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool empty,
                                   const SetupMarkers& marks) {
    const ChromeRects& cr = fl.chrome;

    fillSurface(bmp, toKitBox(cr.toolbar), Role::BgPanel, InteractionState::Rest);

    // Toolbar: product name + live readout. The beta channel gets no distinct accent; the
    // channel-derived vstPluginName is the only beta-vs-stable signal.
    std::string title = version::vstPluginName();
    if (processor_ && processor_->bridge().isConnected()) {
        // The instance's own loaded state outranks bank availability (the bank is a browser
        // source, not the instrument's identity) — a self-contained instance names its sound
        // (refs displayName fallback) even when the bank snapshot is empty.
        if (!selectedId_.empty())
            title += "  [" + sampleLabel(samples_, processor_->sampleRefs(), selectedId_) + "]";
        else if (samples_.empty()) title += "  [bank empty]";
        else title += "  [pick a capture]";
    } else {
        title += "  [host: no bridge]";
    }
    // A bake outcome outranks the identity readout while it lasts: the click's only other
    // feedback is the sound itself, which is by design indistinguishable from before.
    if (bakeMessageTicks_ > 0 && !bakeMessage_.empty()) title = bakeMessage_;
    kitText(bmp, cr.title, title.c_str(), kToolbarFont, Role::TextPrimary);

    // Browse: the picker. When nothing is loaded it is the empty state's dominant
    // call-to-action — draw it Active (accent-primary) so it reads as "start here".
    {
        const KitButtonBox box{toKitBox(cr.navBrowse)};
        const InteractionState st = empty ? InteractionState::Active
            : (isHovered(HoverKind::kNavBrowse, -1) ? InteractionState::Hover
                                                    : InteractionState::Rest);
        drawButton(bmp, box, "Browse", st, /*warn=*/false);
    }

    // The rest of the run and the strip row draw only once a capture is loaded — with
    // nothing picked there is no root, no preview and no channel decision to make.
    if (empty) return;

    // Resample bake. Disabled without the extension: the bank is the extension's surface,
    // so with it absent there is no writer and the control must read unavailable rather
    // than accept a click it cannot honour.
    {
        const KitButtonBox box{toKitBox(cr.bake)};
        const InteractionState st =
            !bakeAvailable_ ? InteractionState::Disabled
                            : (bakePending_ ? InteractionState::Active
                                            : (isHovered(HoverKind::kBake, -1)
                                                   ? InteractionState::Hover
                                                   : InteractionState::Rest));
        drawButton(bmp, box, "Bake", st, /*warn=*/false);
    }

    // Bake Hold: the note length the bake holds the gate for. Drawn ONLY while the window
    // cannot be derived without it — every other dialed sound bakes with no user input, and a
    // control that did nothing there would read as one that did.
    if (bakeHoldNeeded_) {
        const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == kBakeHoldKnobId);
        const bool hov = isHovered(HoverKind::kHoldKnob, -1);
        const InteractionState st = dragging ? InteractionState::Dragging
                                             : (hov ? InteractionState::Hover
                                                    : InteractionState::Rest);
        drawKnobFace(bmp, cr.holdKnob, bakeHoldNorm(), st);
        // "Bake Hold", not "Hold": the AMP deck's AHDSR Hold knob is on screen in the same
        // frame, and two knobs labelled the same are two knobs the user has to guess between.
        const std::string label = dragging || hov
                                      ? instrument::note::divisionLabel(params_.bakeHold)
                                      : std::string("Bake Hold");
        kitTextCentered(bmp, cr.holdLabel, label.c_str(), Font::Micro, Role::TextDim);
    }

    // Preview-trigger button (fires the loaded capture at root through the live voice engine).
    // A drawn play triangle rather than a label or an embedded image: it inherits the button's
    // own foreground role, so it stays legible in every interaction state at no build cost.
    {
        const KitButtonBox box{toKitBox(cr.preview)};
        const bool active = (previewingNote_ >= 0);
        const InteractionState st = active ? InteractionState::Active
            : (isHovered(HoverKind::kPreview, -1) ? InteractionState::Hover
                                                  : InteractionState::Rest);
        drawButton(bmp, box, nullptr, st, /*warn=*/false);
        const PreviewGlyph g = previewGlyph(cr.preview);
        if (!g.empty()) {
            // Same surface as the button label, so the glyph tracks drawButton's own rule
            // (buttonLabelRole) rather than a second copy of it that could desync from the kit.
            const LICE_pixel ink = toLice(roleColor(buttonLabelRole(st)));
            LICE_FillTriangle(bmp, g.leftX, g.topY, g.leftX, g.bottomY, g.apexX, g.apexY,
                              ink, 1.0f, 0);
            // LICE_FillTriangle takes no aa flag, so its two sloped edges come out stepped.
            // Re-stroke them in the same ink: an AA line over the fill's own boundary softens
            // the step without changing the shape. The vertical edge needs none.
            const float lx = static_cast<float>(g.leftX);
            const float ax = static_cast<float>(g.apexX);
            const float ay = static_cast<float>(g.apexY);
            LICE_FLine(bmp, lx, static_cast<float>(g.topY), ax, ay, ink, 1.0f, 0, true);
            LICE_FLine(bmp, lx, static_cast<float>(g.bottomY), ax, ay, ink, 1.0f, 0, true);
        }
    }

    // Preview velocity: a radial knob cell (the deck cell grammar), bound to the same
    // persisted previewVelocity seam. Label swaps to the live value during hover/drag.
    {
        const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == -2);
        const bool hov = isHovered(HoverKind::kVelKnob, -1);
        const InteractionState st = dragging ? InteractionState::Dragging
                                             : (hov ? InteractionState::Hover
                                                    : InteractionState::Rest);
        drawKnobFace(bmp, cr.velKnob, previewVelocity01(), st);
        if (dragging || hov) {
            char buf[8];
            snprintf(buf, sizeof(buf), "%d",
                     static_cast<int>(previewVelocity01() * 127.0 + 0.5));
            kitTextCentered(bmp, cr.velLabel, buf, Font::Micro, Role::TextDim);
        } else {
            kitTextCentered(bmp, cr.velLabel, "Vel", Font::Micro, Role::TextDim);
        }
    }

    // Loop Off | On. A two-segment toggle in the same primitive as Mono|Stereo because it is
    // the same class of control: a playback mode of the loaded capture. Outside Gate both
    // segments draw Disabled and neither accepts a click — the state is preserved, not cleared,
    // so the return to Gate restores it.
    {
        const bool live = loopControlsLive();
        const bool on = marks.hasLoop;
        const auto segState = [&](bool active, HoverKind hk) {
            if (!live) return InteractionState::Disabled;
            if (active) return InteractionState::Active;
            return isHovered(hk, -1) ? InteractionState::Hover : InteractionState::Rest;
        };
        const InteractionState offState = segState(!on, HoverKind::kLoopOff);
        const InteractionState onState = segState(on, HoverKind::kLoopOn);
        fillSurface(bmp, toKitBox(cr.loopOff), Role::BgCell, offState);
        fillSurface(bmp, toKitBox(cr.loopOn), Role::BgCell, onState);
        const Role dim = live ? Role::TextPrimary : Role::TextDim;
        kitTextCentered(bmp, cr.loopOff, "Loop Off", kToolbarFont,
                        (live && !on) ? Role::BgBase : dim);
        kitTextCentered(bmp, cr.loopOn, "Loop On", kToolbarFont,
                        (live && on) ? Role::BgBase : dim);
    }

    // Mono | Stereo output-mode toggle.
    {
        const bool isStereo = (channelMode_ == ChannelMode::Stereo);
        const InteractionState monoState = !isStereo ? InteractionState::Active
            : (isHovered(HoverKind::kChanMono, -1) ? InteractionState::Hover
                                                   : InteractionState::Rest);
        const InteractionState stereoState = isStereo ? InteractionState::Active
            : (isHovered(HoverKind::kChanStereo, -1) ? InteractionState::Hover
                                                     : InteractionState::Rest);
        fillSurface(bmp, toKitBox(cr.chanMono), Role::BgCell, monoState);
        fillSurface(bmp, toKitBox(cr.chanStereo), Role::BgCell, stereoState);
        kitTextCentered(bmp, cr.chanMono, "Mono", kToolbarFont,
                        !isStereo ? Role::BgBase : Role::TextPrimary);
        kitTextCentered(bmp, cr.chanStereo, "Stereo", kToolbarFont,
                        isStereo ? Role::BgBase : Role::TextPrimary);
    }

    // The strip row: the full 128-key piano with the root lit. The loaded capture responds
    // across the whole strip, repitched from that root.
    if (cr.controls.empty() || cr.rootStrip.empty()) return;
    fillSurface(bmp, toKitBox(cr.controls), Role::BgPanel, InteractionState::Rest);
    const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height);
    // Same staleness guard as the tooltip: a latched hover note outlives a drag it started.
    const int hoverNote = (drag_ == DragKind::kNone && hover_.kind == HoverKind::kStripKey)
                              ? hover_.index : -1;
    drawKeyboard(bmp, cr.rootStrip, sl, hoverNote);
    drawRootKey(bmp, cr.rootStrip, sl, effectiveRoot());
}

// Drawn after every band so the chip is never painted over. No hover delay: the strip is a
// continuous readout you sweep, and a delay there reads as a dead surface — unlike the bank
// panel's buttons, where the delay stops tooltips firing on every traverse.
void ReaSamplerEditor::paintChromeTooltip(LICE_IBitmap* bmp, const FaceLayout& fl, int w,
                                          int h) {
    if (hover_.kind != HoverKind::kStripKey || hover_.index < 0) return;
    // Hover is deliberately not re-resolved mid-drag, so the latched note would go stale
    // under a root drag — the root badge is the live readout there.
    if (drag_ != DragKind::kNone) return;
    const Rect& area = fl.chrome.rootStrip;
    if (area.empty()) return;
    const StripLayout sl = layoutStrip(area.width, area.height);
    const Rect key = keyRect(sl, hover_.index);
    if (key.empty()) return;

    const std::string label = noteName(hover_.index);
    const int textW = static_cast<int>(label.size()) * kTooltipCharPx;
    // Anchor y/h to the whole strip row, not the hovered key: a black key (18px) is shorter
    // than a white key (30px), and anchoring to the key rect placed the chip 12px higher for
    // black keys — landing on the keyboard's bottom edge and, near the root, on the badge.
    const TooltipBox tb = computeTooltip(area.x + key.x, area.y, key.width, area.height,
                                         textW, kTooltipTextH, w, h, TooltipSpec{});
    if (tb.empty()) return;

    const Rect box = Rect::ltrb(tb.x, tb.y, tb.x + tb.width, tb.y + tb.height);
    fillSurface(bmp, toKitBox(box), Role::BgCell, InteractionState::Hover);
    LICE_DrawRect(bmp, box.x, box.y, box.width - 1, box.height - 1,
                  toLice(roleColor(Role::LineHairline)), 1.0f, 0);
    kitTextCentered(bmp, box, label.c_str(), Font::Label, Role::TextPrimary);
}

}  // namespace reasampler::vst

#endif  // _WIN32
