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

#include "waveform_view.h"

#include <algorithm>
#include <cstdlib>  // std::abs (int overload)

namespace reasampler::vst {

namespace {

std::int64_t clampFrame(std::int64_t f, std::int64_t frameCount) {
    if (f < 0) return 0;
    if (f > frameCount) return frameCount;
    return f;
}

}  // namespace

int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame) {
    const int w = std::max(0, area.width());
    if (frameCount <= 0 || w <= 0) return area.left;
    const std::int64_t f = clampFrame(frame, frameCount);
    // Linear map: x = left + round(f * w / frameCount). Rounding keeps the marker line
    // visually centered on its frame; the divide is exact rational (multiply first).
    const std::int64_t num = f * static_cast<std::int64_t>(w) + frameCount / 2;
    return area.left + static_cast<int>(num / frameCount);
}

std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x) {
    const int w = std::max(0, area.width());
    if (frameCount <= 0 || w <= 0) return 0;
    if (x <= area.left) return 0;
    if (x >= area.right) return frameCount;
    const std::int64_t dx = static_cast<std::int64_t>(x - area.left);
    // Inverse of frameToX: frame = round(dx * frameCount / w). Round so click and marker draw
    // agree at bin granularity.
    const std::int64_t num = dx * frameCount + static_cast<std::int64_t>(w) / 2;
    return clampFrame(num / static_cast<std::int64_t>(w), frameCount);
}

int markerAtPoint(const Rect& area, std::int64_t frameCount, const std::int64_t* frames,
                  int count, int x, int y) {
    if (count <= 0 || frames == nullptr) return -1;
    if (!contains(area, x, y)) return -1;
    for (int i = 0; i < count; ++i) {
        const int mx = frameToX(area, frameCount, frames[i]);
        if (x >= mx - kMarkerGrabWidth && x <= mx + kMarkerGrabWidth) return i;
    }
    return -1;
}

std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::int64_t startFrame,
                              int dxPixels) {
    const std::int64_t start = clampFrame(startFrame, frameCount);
    if (dxPixels == 0) return start;
    const int w = std::max(0, area.width());
    if (frameCount <= 0 || w <= 0) return start;  // no room to move
    // Proportional shift, rounded to the nearest frame (same linear map as frameToX/xToFrame).
    const std::int64_t magnitude =
        (static_cast<std::int64_t>(std::abs(dxPixels)) * frameCount +
         static_cast<std::int64_t>(w) / 2) /
        static_cast<std::int64_t>(w);
    const std::int64_t shift = dxPixels > 0 ? magnitude : -magnitude;
    return clampFrame(start + shift, frameCount);
}

std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames,
                                 std::int64_t target) {
    if (pcm == nullptr || frames < 2) return clampFrame(target, frames > 0 ? frames - 1 : 0);
    // Clamp target into a valid sample index [0, frames).
    std::int64_t t = target;
    if (t < 0) t = 0;
    if (t > frames - 1) t = frames - 1;

    // A crossing lives at frame i (1 <= i < frames) when sign(pcm[i-1]) != sign(pcm[i]) OR
    // pcm[i] == 0. isCrossing(i) tests exactly that. We fan out from t: at each distance d we
    // probe t-d before t+d, so an equidistant tie resolves to the LOWER frame (deterministic).
    auto isCrossing = [&](std::int64_t i) -> bool {
        if (i < 1 || i >= frames) return false;
        const AudioSample a = pcm[i - 1];
        const AudioSample b = pcm[i];
        if (b == 0.0f) return true;                  // a sample on zero is its own crossing
        return (a < 0.0f) != (b < 0.0f);             // sign change between i-1 and i
    };

    if (isCrossing(t)) return t;
    for (std::int64_t d = 1; d < frames; ++d) {
        const std::int64_t lo = t - d;
        if (lo >= 1 && isCrossing(lo)) return lo;    // lower side wins the tie
        const std::int64_t hi = t + d;
        if (hi < frames && isCrossing(hi)) return hi;
        // Stop once both probes have run off both ends — no crossing anywhere.
        if (lo < 1 && hi >= frames) break;
    }
    return t;  // no sign change in the whole buffer -> keep the raw (clamped) target
}

}  // namespace reasampler::vst
