// editor_paint_deck.cpp — the DECKS band's painter: the fenced control groups, their
// captions, the compact caption and row toggles, and the radial knobs with the label<->value
// swap on hover/drag. Windows-only; the deck's cell geometry is the pure knob_deck layout and
// its group composition the pure deck_groups list.

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32

#include <string>
#include <vector>

#include "core/instrument/engine/filter/filter_morph.h"  // MorphLaw (the law toggle's state)
#include "core/instrument/ui/knob_deck.h"       // deck layout + kDeckKnobSize
#include "core/instrument/ui/master_meter.h"    // the bus meter's column interior + ballistics
#include "core/instrument/ui/waveform_view.h"   // resolveLaneSplit (THE lane-split fold)
#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;  // deck geometry

// The knob's own name/value band. One size up from the group captions and the toggles, which
// are chrome you read once — this is the readout you read while turning something.
constexpr Font kCellLabelFont = Font::Label;

namespace {

// One tick's numeral, in whole dB ("0", "-12"). No unit suffix — the column is 22px wide and
// the scale's unit is stated once, by the caption.
std::string tickLabel(int db) {
    char buf[8];
    std::snprintf(buf, sizeof(buf), "%d", db);
    return std::string(buf);
}

// The numeral gutter, left of the bars: static content, so it is drawn on a full paint only —
// re-blending AA text onto itself every meter frame would thicken it.
void drawMeterNumerals(LICE_IBitmap* bmp, const MeterRects& m) {
    if (m.field.empty()) return;
    for (int db = static_cast<int>(instrument::engine::kMeterTopDb);
         db >= static_cast<int>(instrument::engine::kMeterFloorDb);
         db -= static_cast<int>(kMeterTickStepDb)) {
        if (!meterTickNumeralled(db)) continue;
        kitText(bmp, meterNumeralRect(m.labels, meterDbToY(m.field, db)), tickLabel(db).c_str(),
                Font::Micro, Role::TextDim, Align::Right);
    }
}

// The bar field: the dB rules, one or two bars, the held peak tick and the latched clip cap —
// everything in the column that moves at meter rate. SELF-CONTAINED on purpose: the BgCell fill
// covers every pixel the rest of it then draws, so a meter frame can redraw this rect alone.
// Two bars or one is read off the rects (barB is empty exactly when the split is Single), so
// the lane decision has one representation here rather than two.
// This self-containment depends on Role::BgCell being fully OPAQUE at InteractionState::Rest
// (theme.cpp: alpha 255) — fillGradient blends rather than overwrites at alpha < 255, so a
// translucent BgCell would make every meter frame re-blend over whatever the last frame left,
// the exact AA-thickening the numeral gutter comment below is guarding against.
void drawMeterField(LICE_IBitmap* bmp, const MeterRects& m, const MasterMeterUi& state) {
    // A column narrower than the interior needs yields all-empty rects, which under rect.h's
    // contract means suppressed — not a zero-height field to fill, tick twelve times and cap.
    if (m.field.empty()) return;
    fillSurface(bmp, toKitBox(m.field), Role::BgCell, InteractionState::Rest);

    // Scale: a rule every 6 dB, numeralled every 12 with 0 dB heavier — the reference the
    // limiter-off case is read against.
    const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
    for (int db = static_cast<int>(instrument::engine::kMeterTopDb);
         db >= static_cast<int>(instrument::engine::kMeterFloorDb);
         db -= static_cast<int>(kMeterTickStepDb)) {
        const int y = meterDbToY(m.field, db);
        const bool zero = (db == 0);
        LICE_FillRect(bmp, m.field.x, y, m.field.width, zero ? 2 : 1,
                      zero ? toLice(roleColor(Role::TextDim)) : hairline, 1.0f, 0);
    }

    // The bars. A single-lane surface shows ONE bar folding both channels per field
    // (meterSingleLaneState) — the two are the same signal there (dual-mono), so two bars would
    // be a duplicate rather than a reading.
    const LICE_pixel barInk = toLice(roleColor(Role::AccentPrimary));
    const LICE_pixel holdInk = toLice(roleColor(Role::TextPrimary));
    const auto drawBar = [&](const Rect& bar, const instrument::engine::MeterState& ch) {
        if (bar.empty()) return;
        const int top = meterDbToY(bar, ch.levelDb);
        if (top < bar.bottom()) {
            LICE_FillRect(bmp, bar.x, top, bar.width, bar.bottom() - top, barInk, 1.0f, 0);
        }
        if (ch.holdDb > instrument::engine::kMeterFloorDb) {
            // Clamped so the 2px tick cannot hang past the bar when the hold sits on the floor.
            const int hold = (std::min)(meterDbToY(bar, ch.holdDb), bar.bottom() - 2);
            LICE_FillRect(bmp, bar.x, hold, bar.width, 2, holdInk, 1.0f, 0);
        }
    };
    if (m.barB.empty()) {
        drawBar(m.barA, meterSingleLaneState(state));
    } else {
        drawBar(m.barA, state.left);
        drawBar(m.barB, state.right);
    }

    // The clip cap: latched over the whole field, click to clear.
    if (meterClipped(state)) {
        LICE_FillRect(bmp, m.field.x, m.field.y, m.field.width, 3,
                      toLice(roleColor(Role::Warn)), 1.0f, 0);
    }
}

}  // namespace

void ReaSamplerEditor::paintMeterField(LICE_IBitmap* bmp) {
    drawMeterField(bmp, meterRects_, masterMeter_);
}

void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
    const Rect& deckArea = fl.bands.decks;
    if (deckArea.width <= 0 || deckArea.height <= 0) return;
    const DeckLayout dl = layoutDeck(fl.deckDescs, deckArea.x, deckArea.y, deckArea.width);
    const PlaySeconds& play = params_.play;
    const bool isMono = (voiceMode_ == VoiceMode::Mono);
    const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
    // The meter's bar count is the SAME resolved decision the waveform's lane split is —
    // resolveLaneSplit is the one home of it. Asked directly rather than read back off
    // waveformSurface, whose laneCount additionally folds in the waveform BAND's pixel height,
    // which decides nothing about how many channels the bus is carrying.
    const LaneSplit meterSplit = resolveLaneSplit(channelMode_ == ChannelMode::Stereo,
                                                  channelPcmFor(selectedId_).channelCount);

    // One compact-toggle draw (the Mono/Stereo segment grammar at Micro scale). Disabled
    // segments draw inert so the dependency (Retrig|Legato needs Mono) reads at a glance.
    // `seg0Disabled` disables ONE segment: Gate is unselectable while an EG is drawn, but
    // Trigger — the mode it is stuck in — must still read as the live choice.
    const auto drawToggle = [&](const DeckToggleLayout& t, const char* s0, const char* s1,
                                bool seg1Active, bool disabled, bool seg0Disabled = false) {
        const bool hov = !disabled && isHovered(HoverKind::kControl, t.id);
        const bool d0 = disabled || seg0Disabled;
        const InteractionState st0 =
            d0 ? InteractionState::Disabled
               : (!seg1Active ? InteractionState::Active
                              : (hov ? InteractionState::Hover : InteractionState::Rest));
        const InteractionState st1 =
            disabled ? InteractionState::Disabled
                     : (seg1Active ? InteractionState::Active
                                   : (hov ? InteractionState::Hover : InteractionState::Rest));
        fillSurface(bmp, toKitBox(t.seg0), Role::BgCell, st0);
        fillSurface(bmp, toKitBox(t.seg1), Role::BgCell, st1);
        kitTextCentered(bmp, t.seg0, s0, Font::Micro,
                        d0 ? Role::TextDim
                           : (!seg1Active ? Role::BgBase : Role::TextPrimary));
        kitTextCentered(bmp, t.seg1, s1, Font::Micro,
                        disabled ? Role::TextDim
                                 : (seg1Active ? Role::BgBase : Role::TextPrimary));
    };
    const bool anySpline = splineActive(play);

    // The knob's short name label (swapped for the live value during hover/drag — no third
    // line, no permanent value clutter).
    const auto knobName = [](ParamControl c) -> const char* {
        switch (c) {
            case ParamControl::kAttack:         return "Attack";
            case ParamControl::kHold:           return "Hold";
            case ParamControl::kDecay:          return "Decay";
            case ParamControl::kSustain:        return "Sustain";
            case ParamControl::kRelease:        return "Release";
            case ParamControl::kTrigLength:     return "Len %";
            case ParamControl::kTrigAttack:     return "Attack";
            case ParamControl::kTrigHold:       return "Hold";
            case ParamControl::kTrigDecay:      return "Decay";
            case ParamControl::kKeyTrack:       return "Key Trk";
            case ParamControl::kRate:           return "Rate";
            case ParamControl::kPitch:          return "Pitch";
            case ParamControl::kPitchEnvAttack: return "P.Att";
            case ParamControl::kPitchEnvHold:   return "P.Hold";
            case ParamControl::kPitchEnvDecay:  return "P.Dec";
            case ParamControl::kPitchEnvDepth:  return "P.Depth";
            case ParamControl::kVoiceCount:     return "Voices";
            case ParamControl::kMasterGain:     return "Gain";
            case ParamControl::kFilterMorph:    return "Mode";
            case ParamControl::kFilterCutoff:   return "Cutoff";
            case ParamControl::kFilterQ:        return "Res";
            case ParamControl::kFilterDrive:    return "Drive";
            case ParamControl::kFilterModAmt:   return "Mod";
            case ParamControl::kFilterVel:      return "Vel";
            case ParamControl::kFilterKeyTrack: return "Key Trk";
            case ParamControl::kAmpVelCurve:    return "Amp";
            case ParamControl::kPitchVelCurve:  return "Pitch";
            case ParamControl::kFilterVelCurve: return "Filter";
            case ParamControl::kFilterEnvAttack:  return "F.Att";
            case ParamControl::kFilterEnvHold:    return "F.Hold";
            case ParamControl::kFilterEnvDecay:   return "F.Dec";
            case ParamControl::kFilterEnvSustain: return "F.Sus";
            case ParamControl::kFilterEnvRelease: return "F.Rel";
            case ParamControl::kFilterTrigAttack: return "F.Att";
            case ParamControl::kFilterTrigHold:   return "F.Hold";
            case ParamControl::kFilterTrigDecay:  return "F.Dec";
            default:                            return "";
        }
    };

    for (const DeckGroupLayout& g : dl.groups) {
        // The fence: a bg/panel box with a hairline border, caption micro-caps left.
        fillSurface(bmp, toKitBox(g.box), Role::BgPanel, InteractionState::Rest);
        LICE_DrawRect(bmp, g.box.x, g.box.y, g.box.width - 1, g.box.height - 1,
                      hairline, 1.0f, 0);
        const char* caption = "";
        switch (g.id) {
            case kGroupAmpEnv:    caption = "AMP ENVELOPE"; break;
            case kGroupPitch:     caption = "PITCH/RATE"; break;
            case kGroupPitchEnv:  caption = "PITCH ENV"; break;
            case kGroupFilter:    caption = "FILTER"; break;
            case kGroupFilterEnv: caption = "FILTER ENV"; break;
            case kGroupVelocity:  caption = "VELOCITY"; break;
            case kGroupVoice:     caption = "VOICE"; break;
            case kGroupMaster:    caption = "MASTER"; break;
            default: break;
        }
        kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim);

        // The gain-reduction lamp. ROUND, where the overlay radios in this same slot are
        // square, so it reads as a lamp rather than a control.
        if (g.captionRadio.id >= 0 && g.captionRadio.passive) {
            const Rect& rb = g.captionRadio.box;
            const float r = rb.width / 2.0f - 0.5f;
            LICE_FillCircle(bmp, rb.x + rb.width / 2.0f, rb.y + rb.height / 2.0f, r,
                            toLice(roleColor(grLampLit(masterMeter_) ? Role::Warn
                                                                     : Role::LineHairline)),
                            1.0f, 0, true);
        }
        // The overlay-select radio: filled in the tertiary accent (the colour the overlay
        // traces in) when this group's envelope is the one on the waveform, hollow otherwise.
        if (g.captionRadio.id >= 0 && !g.captionRadio.passive) {
            // overlayEnvForRadio returns kNone for BOTH "not a radio id" and "no selection" —
            // a non-radio id must never read as lit just because nothing is selected, so the
            // picked env has to be checked against kNone itself, not just matched by equality.
            const OverlayEnv picked = overlayEnvForRadio(g.captionRadio.id);
            const bool on = picked != OverlayEnv::kNone && overlayEnv_ == picked;
            const bool hov = isHovered(HoverKind::kEnvRadio, g.captionRadio.id);
            const Rect& rb = g.captionRadio.box;
            LICE_DrawRect(bmp, rb.x, rb.y, rb.width - 1, rb.height - 1,
                          toLice(roleColor(on || hov ? Role::AccentTertiary
                                                     : Role::LineHairline)),
                          1.0f, 0);
            if (on) {
                LICE_FillRect(bmp, rb.x + 3, rb.y + 3, rb.width - 6, rb.height - 6,
                              toLice(roleColor(Role::AccentTertiary)), 1.0f, 0);
            }
        }

        // The compact caption toggles (right-anchored in the caption row, never full-width).
        for (const DeckToggleLayout* tp : {&g.captionToggle, &g.captionToggle2}) {
            if (tp->id < 0) continue;
            const DeckToggleLayout& t = *tp;
            switch (static_cast<ParamControl>(t.id)) {
                case ParamControl::kPlayMode:
                    drawToggle(t, "Gate", "Trigger", play.playMode == PlayMode::Trigger, false,
                               /*seg0Disabled=*/anySpline);
                    break;
                case ParamControl::kPitchEngine:
                    drawToggle(t, "Varisp", "Presrv",
                               play.pitchEngine == PitchEngine::Preserve, false);
                    break;
                case ParamControl::kPitchEnvEnable:
                    drawToggle(t, "Off", "On", play.pitchEnv.enabled, false);
                    break;
                case ParamControl::kVoiceMode:
                    drawToggle(t, "Poly", "Mono", isMono, false);
                    break;
                case ParamControl::kFilterEnable:
                    drawToggle(t, "Off", "On", play.filter.enabled, false);
                    break;
                case ParamControl::kFilterLaw:
                    drawToggle(t, "Band", "Notch",
                               play.filter.settings.morphLaw ==
                                   instrument::engine::filter::MorphLaw::HighNotchLow,
                               !play.filter.enabled);
                    break;
                case ParamControl::kLimiterEnable:
                    drawToggle(t, "Off", "On", params_.limiterEnabled, false);
                    break;
                case ParamControl::kAmpEnvMode:
                    drawToggle(t, "Stg", "Spl", play.ampSpline.mode == EnvMode::Spline, false);
                    break;
                case ParamControl::kPitchEnvMode:
                    drawToggle(t, "Stg", "Spl", play.pitchSpline.mode == EnvMode::Spline, false);
                    break;
                case ParamControl::kFilterEnvMode:
                    drawToggle(t, "Stg", "Spl", play.filterSpline.mode == EnvMode::Spline, false);
                    break;
                default: break;
            }
        }
        // The one row toggle left: VOICE's Retrig|Legato, live only in Mono.
        if (g.rowToggle.id >= 0) {
            drawToggle(g.rowToggle, "Retrig", "Legato",
                       monoTrigger_ == MonoTrigger::Legato, !isMono);
        }

        if (g.column.id >= 0) {
            // Cached so the meter's own tick repaints the field without re-laying out the deck.
            meterRects_ = meterRects(g.column.box, meterSplit);
            drawMeterNumerals(bmp, meterRects_);
            drawMeterField(bmp, meterRects_, masterMeter_);
        }

        // The knobs. A dependent group's knobs draw Disabled (not hidden) — stable geometry.
        // The predicate is the input side's, so the drawn state and the inert grab agree.
        for (const DeckCellLayout& c : g.cells) {
            const bool disabled = deckKnobDisabled(c.id);
            // A VELOCITY cell is a popup opener, not a dial: it shows its curve in miniature
            // where a knob face would be, and its whole cell is the click target.
            const CurveTarget curveCell = curveTargetFor(c.id);
            if (curveCell != CurveTarget::kNone) {
                paintCurveButton(bmp, c.knob, curveCell, disabled,
                                 !disabled && isHovered(HoverKind::kControl, c.id));
                kitTextCentered(bmp, c.label, knobName(static_cast<ParamControl>(c.id)),
                                kCellLabelFont, Role::TextDim);
                continue;
            }
            const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == c.id);
            const bool hov = !disabled && isHovered(HoverKind::kControl, c.id);
            const InteractionState st =
                disabled ? InteractionState::Disabled
                         : (dragging ? InteractionState::Dragging
                                     : (hov ? InteractionState::Hover : InteractionState::Rest));
            drawKnobFace(bmp, c.knob, deckControlNorm(c.id), st);

            // The inner dial rides only the knobs whose stage is sloped — deck_groups owns
            // that rule, so a Hold or Sustain cell simply has no curve id and draws none.
            const ParamControl curve = curveParamFor(static_cast<ParamControl>(c.id));
            const bool innerDragging =
                (drag_ == DragKind::kDeckKnob && dragInnerCellId_ == c.id);
            const bool innerHov = !disabled && isHovered(HoverKind::kInnerDial, c.id);
            if (curve != ParamControl::kCount) {
                const InteractionState ist =
                    disabled ? InteractionState::Disabled
                             : (innerDragging ? InteractionState::Dragging
                                              : (innerHov ? InteractionState::Hover
                                                          : InteractionState::Rest));
                drawInnerDial(bmp, c.inner, deckControlNorm(static_cast<int>(curve)), ist);
            }

            // One label band, so the inner dial's readout takes it while the inner dial is the
            // one being touched.
            std::string label;
            if (innerDragging || innerHov) label = deckValueLabel(static_cast<int>(curve));
            else if (dragging || hov) label = deckValueLabel(c.id);
            else label = std::string(knobName(static_cast<ParamControl>(c.id)));
            kitTextCentered(bmp, c.label, label.c_str(), kCellLabelFont, Role::TextDim);
        }
    }
}

}  // namespace reasampler::vst

#endif  // _WIN32
