// note_program.cpp — see note_program.h. Pure; standard library only.

#include "core/instrument/note/note_program.h"

#include <algorithm>
#include <cmath>

namespace reasampler::instrument::note {

Velocity Velocity::of(int value) {
    Velocity v;
    v.value_ = static_cast<std::uint8_t>((std::max)(kMin, (std::min)(kMax, value)));
    return v;
}

bool operator==(Velocity a, Velocity b) { return a.value() == b.value(); }

bool operator==(OffsetAmount a, OffsetAmount b) {
    return a.magnitude() == b.magnitude() && a.denomination() == b.denomination();
}

bool operator!=(OffsetAmount a, OffsetAmount b) { return !(a == b); }

OffsetAmount offsetOf(double magnitude, Denomination denomination) {
    const double bounded =
        std::isnan(magnitude) ? 0.0
                              : (std::max)(-kMaxConvertibleMagnitude,
                                           (std::min)(kMaxConvertibleMagnitude, magnitude));
    const bool named = denomination == Denomination::Milliseconds
                       || denomination == Denomination::Beats;
    return OffsetAmount(bounded, named ? denomination : Denomination::Milliseconds);
}

NoteLength lengthOfDivision(Division division) { return NoteLength(division, 0.0, false); }

NoteLength lengthOfSeconds(double seconds) {
    const double bounded = std::isnan(seconds)
                               ? 0.0
                               : (std::max)(0.0, (std::min)(kMaxLengthSeconds, seconds));
    return NoteLength(Division{}, bounded, true);
}

bool operator==(NoteLength a, NoteLength b) {
    if (a.exact_ != b.exact_) return false;
    return a.exact_ ? a.seconds_ == b.seconds_ : a.division_ == b.division_;
}

bool operator!=(NoteLength a, NoteLength b) { return !(a == b); }

double noteLengthSeconds(NoteLength length, Tempo tempo) {
    return length.exact_ ? length.seconds_ : tempo.beatsToSeconds(divisionBeats(length.division_));
}

OffsetAmount offsetFromMs(double ms) { return offsetOf(ms, Denomination::Milliseconds); }

OffsetAmount offsetFromBeats(double beats) { return offsetOf(beats, Denomination::Beats); }

// Milliseconds is pinned AFTER the switch rather than by a `default:` inside it, so the
// switch stays exhaustive over the enum and a third denomination trips switch-exhaustiveness
// diagnostics here instead of silently resolving as ms in all three. Those diagnostics are
// off at this project's warning level, so read it as a signpost — the tests are the gate.
double offsetMs(OffsetAmount amount, Tempo tempo) {
    switch (amount.denomination()) {
        case Denomination::Beats: return tempo.beatsToMs(amount.magnitude());
        case Denomination::Milliseconds: break;
    }
    return amount.magnitude();
}

double offsetBeats(OffsetAmount amount, Tempo tempo) {
    switch (amount.denomination()) {
        case Denomination::Beats: return amount.magnitude();
        case Denomination::Milliseconds: break;
    }
    return tempo.msToBeats(amount.magnitude());
}

double offsetSeconds(OffsetAmount amount, Tempo tempo) {
    switch (amount.denomination()) {
        case Denomination::Beats: return tempo.beatsToSeconds(amount.magnitude());
        case Denomination::Milliseconds: break;
    }
    return msToSeconds(amount.magnitude());
}

OffsetAmount redenominate(OffsetAmount amount, Denomination to, Tempo tempo) {
    // Defense-in-depth, not a discriminating guard: the branch below already treats any
    // non-Beats target as Milliseconds, so an unnamed `to` resolves the same way whether or
    // not it is routed through offsetOf first. Kept because a future third denomination
    // would make this the one place that still pins it.
    const Denomination target = offsetOf(0.0, to).denomination();
    if (amount.denomination() == target) return amount;
    return target == Denomination::Beats ? offsetFromBeats(offsetBeats(amount, tempo))
                                         : offsetFromMs(offsetMs(amount, tempo));
}

OffsetAmount withMsView(OffsetAmount amount, double ms, Tempo tempo) {
    return amount.denomination() == Denomination::Beats ? offsetFromBeats(tempo.msToBeats(ms))
                                                        : offsetFromMs(ms);
}

OffsetAmount withBeatsView(OffsetAmount amount, double beats, Tempo tempo) {
    return amount.denomination() == Denomination::Beats
               ? offsetFromBeats(beats)
               : offsetFromMs(tempo.beatsToMs(beats));
}

bool operator==(const NoteProgram& a, const NoteProgram& b) {
    return a.length == b.length && a.start.amount() == b.start.amount()
           && a.end.amount() == b.end.amount() && a.velocity == b.velocity;
}

bool operator!=(const NoteProgram& a, const NoteProgram& b) { return !(a == b); }

ResolvedNote resolveNote(const NoteProgram& program, Tempo tempo) {
    ResolvedNote out;
    out.noteOffSeconds = noteLengthSeconds(program.length, tempo);
    out.captureStartSeconds = offsetSeconds(program.start.amount(), tempo);
    const double rawEndSeconds = out.noteOffSeconds + offsetSeconds(program.end.amount(), tempo);
    // An inverted window has no meaning to a renderer, so a far-negative end offset yields a
    // zero-length capture the caller can reject rather than a negative one it cannot.
    out.windowCollapsed = rawEndSeconds < out.captureStartSeconds;
    out.captureEndSeconds = (std::max)(rawEndSeconds, out.captureStartSeconds);
    out.velocity = program.velocity.value();
    return out;
}

}  // namespace reasampler::instrument::note
