// keyboard_strip.cpp — see keyboard_strip.h. Pure math; no host types.

#include "keyboard_strip.h"

#include <algorithm>

namespace reasampler::vst {

namespace {

int clampNote(int n) {
    if (n < 0) return 0;
    if (n > kStripKeyCount - 1) return kStripKeyCount - 1;
    return n;
}

// Map a key BOUNDARY in [0, kStripKeyCount] to an x pixel inside a band of the given
// left/width. keyEdge is a boundary (0..128): 0 -> band left, 128 -> band right. Integer
// math, floored — key N's left is keyEdgeToX(N) and its right is keyEdgeToX(N+1), tiling
// adjacent keys/zones without a seam (mirror of embed_strip::keyEdgeToX).
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
    if (keyEdge <= 0) return bandLeft;
    if (keyEdge >= kStripKeyCount) return bandLeft + bandWidth;
    return bandLeft + (keyEdge * bandWidth) / kStripKeyCount;
}

}  // namespace

StripLayout layoutStrip(int w, int h) {
    const int cw = std::max(0, w);
    const int ch = std::max(0, h);
    StripLayout out;
    out.keys = Rect{0, 0, cw, ch};
    return out;
}

int keyLeftX(const StripLayout& layout, int note) {
    const Rect& band = layout.keys;
    const int bandWidth = std::max(0, band.width());
    // note is a KEY here (0..127); its left edge is boundary `note`. Callers pass note+1 to
    // get a key's right edge, and 128 maps to the band right.
    const int edge = note < 0 ? 0 : (note > kStripKeyCount ? kStripKeyCount : note);
    return keyEdgeToX(band.left, bandWidth, edge);
}

Rect keyRect(const StripLayout& layout, int note) {
    const int n = clampNote(note);
    const int leftX = keyLeftX(layout, n);
    const int rightX = keyLeftX(layout, n + 1);
    return Rect{leftX, layout.keys.top, std::max(leftX, rightX), layout.keys.bottom};
}

Rect rootMarkerRect(const StripLayout& layout, int rootNote) {
    return keyRect(layout, rootNote);
}

int keyAtPoint(const StripLayout& layout, int x, int y) {
    const Rect& band = layout.keys;
    if (!contains(band, x, y)) return -1;
    const int bandWidth = std::max(0, band.width());
    if (bandWidth <= 0) return -1;
    // Invert keyEdgeToX: the key whose half-open [leftX, rightX) contains x. Floor-divide
    // the pixel offset back to a key; clamp defensively (a point on band.right-1 maps to 127).
    const int offset = x - band.left;
    int note = (offset * kStripKeyCount) / bandWidth;
    return clampNote(note);
}

Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote) {
    int lo = clampNote(lowNote);
    int hi = clampNote(highNote);
    if (lo > hi) lo = hi;  // defensive: a malformed zone collapses rather than inverts
    const int leftX = keyLeftX(layout, lo);
    const int rightX = keyLeftX(layout, hi + 1);
    return Rect{leftX, layout.keys.top, std::max(leftX, rightX), layout.keys.bottom};
}

ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y) {
    const Rect bar = zoneBarRect(layout, lowNote, highNote);
    if (!contains(bar, x, y)) return ZoneGrab::kNone;

    const int barW = bar.width();
    // A narrow bar (< 2*edge) has no body: split at the midpoint, LOW edge wins the tie so
    // a click exactly on the midpoint resizes low (deterministic).
    if (barW < 2 * kStripEdgeGrabWidth) {
        const int mid = bar.left + barW / 2;
        return x <= mid ? ZoneGrab::kLowEdge : ZoneGrab::kHighEdge;
    }
    if (x < bar.left + kStripEdgeGrabWidth) return ZoneGrab::kLowEdge;
    if (x >= bar.right - kStripEdgeGrabWidth) return ZoneGrab::kHighEdge;
    return ZoneGrab::kBody;
}

ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
                          int count, int x, int y) {
    if (count <= 0 || lows == nullptr || highs == nullptr) return ZoneBarHit{};
    if (!contains(layout.keys, x, y)) return ZoneBarHit{};
    for (int i = 0; i < count; ++i) {
        const ZoneGrab g = zoneGrabAt(layout, lows[i], highs[i], x, y);
        if (g != ZoneGrab::kNone) return ZoneBarHit{i, g};
    }
    return ZoneBarHit{};  // on the band but on no bar
}

bool isNaturalKey(int note) {
    // Clamp to the valid MIDI range before indexing.
    const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
    // The 12-semitone pattern of natural (white) keys within an octave, starting at C:
    //   positions 0(C) 2(D) 4(E) 5(F) 7(G) 9(A) 11(B) are natural;
    //   positions 1(C#) 3(D#) 6(F#) 8(G#) 10(A#) are accidental.
    static constexpr bool kNatural[12] = {
        true,   // 0  C
        false,  // 1  C#
        true,   // 2  D
        false,  // 3  D#
        true,   // 4  E
        true,   // 5  F
        false,  // 6  F#
        true,   // 7  G
        false,  // 8  G#
        true,   // 9  A
        false,  // 10 A#
        true,   // 11 B
    };
    return kNatural[n % 12];
}

int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) {
    if (dxPixels == 0) return clampNote(startNote);
    const int bandWidth = std::max(0, layout.keys.width());
    if (bandWidth <= 0) return clampNote(startNote);  // zero-width -> no motion
    // Proportional shift: same linear mapping as keyAtPoint/keyEdgeToX so click and drag
    // agree across the full strip, even on non-divisible-by-128 widths. The proportional
    // key width is (bandWidth / kStripKeyCount) in exact rational arithmetic; rounding to
    // the nearest key (half-key drag flips at the key centre) is achieved by adding
    // bandWidth/2 to the absolute pixel delta before dividing — identical to the old
    // formula except keyWidth is now derived from the same linear map (exact rational)
    // rather than the truncated-integer bandWidth/128 that caused drift at the far end.
    const int half = bandWidth / 2;
    int shift;
    if (dxPixels > 0) {
        shift = (dxPixels * kStripKeyCount + half) / bandWidth;
    } else {
        shift = -(((-dxPixels) * kStripKeyCount + half) / bandWidth);
    }
    return clampNote(startNote + shift);
}

}  // namespace reasampler::vst
