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

#include "core/instrument/ui/waveform_view.h"

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

#include "core/instrument/ui/sample_bands.h"  // waveformLanes (the band's lane inventory)

namespace reasampler::instrument::ui {

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

OverlayArea waveformOverlayArea(const Rect& band) {
    return OverlayArea{band.empty() ? Rect{} : band};
}

LaneSplit resolveLaneSplit(bool stereoMode, int sourceChannels) {
    return (stereoMode && sourceChannels >= 2) ? LaneSplit::Stereo : LaneSplit::Single;
}

WaveformSurface waveformSurface(const Rect& band, bool stereoMode, int sourceChannels) {
    WaveformSurface s;
    if (band.empty()) return s;
    s.overlay = waveformOverlayArea(band);
    const WaveformLanes lanes = waveformLanes(band, resolveLaneSplit(stereoMode, sourceChannels));
    s.upper = lanes.upper;
    s.lower = lanes.lower;
    // Derived from the resolved lanes, not `twoLanes` — a stereo split's integer division
    // rounds the lower lane to empty for a band this thin (height <= 3), far below the
    // allocator's kWaveformMinHeight floor but reachable if this is called directly with an
    // arbitrary rect (as tests do).
    s.laneCount = lanes.lower.empty() ? 1 : 2;
    return s;
}

audio::Envelope laneEnvelope(const audio::Envelope& env, int lane) {
    if (lane < 0 || static_cast<std::size_t>(lane) >= env.size()) return {};
    return audio::Envelope{env[static_cast<std::size_t>(lane)]};
}

int frameToX(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame) {
    const int w = std::max(0, area.rect.width);
    if (frameCount <= 0 || w <= 0) return area.rect.x;
    const std::int64_t f = clampFrame(frame, frameCount);
    // x = left + round(f * w / frameCount); multiply before divide to keep this exact.
    const std::int64_t num = f * static_cast<std::int64_t>(w) + frameCount / 2;
    return area.rect.x + static_cast<int>(num / frameCount);
}

std::int64_t xToFrame(const OverlayArea& area, std::int64_t frameCount, int x) {
    const Rect& r = area.rect;
    const int w = std::max(0, r.width);
    if (frameCount <= 0 || w <= 0) return 0;
    if (x <= r.x) return 0;
    if (x >= r.right()) return frameCount;
    const std::int64_t dx = static_cast<std::int64_t>(x - r.x);
    // Inverse of frameToX: frame = round(dx * frameCount / w).
    const std::int64_t num = dx * frameCount + static_cast<std::int64_t>(w) / 2;
    return clampFrame(num / static_cast<std::int64_t>(w), frameCount);
}

Rect markerHandleRect(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame) {
    const Rect& r = area.rect;
    if (r.empty()) return Rect{};
    const int mx = frameToX(area, frameCount, frame);
    // Clip into the area: a marker at the last frame maps to right(), whose unclipped tab
    // would claim pixels outside the band the caller already hit-tested.
    const int left = std::max(r.x, mx - kMarkerHandleHalfWidth);
    const int right = std::min(r.right(), mx + kMarkerHandleHalfWidth + 1);
    if (right <= left) return Rect{};
    return Rect{left, r.y, right - left, std::min(kMarkerHandleHeight, r.height)};
}

int capAtPoint(const OverlayArea& area, std::int64_t frameCount, const WaveMarks& marks, int x,
               int y) {
    for (int i = kWaveMarkCount - 1; i >= 0; --i) {
        if (!marks.present[i]) continue;
        if (contains(markerHandleRect(area, frameCount, marks.frame[i]), x, y)) return i;
    }
    return -1;
}

bool markLabelLeftOfLine(WaveMark m) {
    return m == WaveMark::kLoopEnd || m == WaveMark::kCrossfade;
}

Rect markLabelRect(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame,
                   bool leftOfLine, int textWidth) {
    const Rect& r = area.rect;
    if (r.empty() || textWidth <= 0 || textWidth > r.width) return Rect{};
    const int h = std::min(kMarkLabelHeight, r.height - kMarkerHandleHeight);
    if (h <= 0) return Rect{};
    const int mx = frameToX(area, frameCount, frame);
    int left = leftOfLine ? mx - kMarkLabelGap - textWidth : mx + kMarkLabelGap;
    left = std::max(r.x, std::min(left, r.right() - textWidth));
    return Rect{left, r.y + kMarkerHandleHeight, textWidth, h};
}

WaveMarkLabels layoutMarkLabels(const OverlayArea& area, std::int64_t frameCount,
                                const WaveMarks& marks, const int* textWidth, int promoted) {
    WaveMarkLabels out;
    if (textWidth == nullptr) return out;
    // The promoted mark first, then draw order. Every label shares one row, so "would overlap
    // one already placed" reduces to a horizontal span test.
    int order[kWaveMarkCount + 1] = {promoted, 0, 1, 2, 3};
    for (int slot = 0; slot < kWaveMarkCount + 1; ++slot) {
        const int i = order[slot];
        if (i < 0 || i >= kWaveMarkCount) continue;
        if (!marks.present[i] || !out.box[i].empty()) continue;
        const Rect box = markLabelRect(area, frameCount, marks.frame[i],
                                       markLabelLeftOfLine(static_cast<WaveMark>(i)),
                                       textWidth[i]);
        if (box.empty()) continue;
        bool clash = false;
        for (int j = 0; j < kWaveMarkCount && !clash; ++j) {
            clash = !out.box[j].empty() && box.x < out.box[j].right() &&
                    out.box[j].x < box.right();
        }
        if (!clash) out.box[i] = box;
    }
    return out;
}

int crossfadeWedgeHeight(int x0, int x1, int x) {
    const int w = x1 - x0;
    if (w <= 0 || x < x0 || x >= x1) return 0;
    if (w == 1) return kCrossfadeWedgePx;
    // Normalized over w - 1 so the LAST drawn column lands exactly on the peak, the same
    // reason crossfadeWeight normalizes over crossfade - 1 (loop_span.h).
    const int d = x - x0;
    return (kCrossfadeWedgePx * d + (w - 1) / 2) / (w - 1);
}

int markerAtPoint(const OverlayArea& 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.rect, 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 OverlayArea& 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.rect.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::instrument::ui
