// note_program — the programmed capture signal: one note length, one velocity, two anchored
// offsets, and the one resolver a preview and a bake must share.
//
// Resolved times are rate-free seconds (this directory's CLAUDE.md: the standing ruling).

#pragma once

#include <cstdint>
#include <type_traits>

#include "core/instrument/note/musical_division.h"
#include "core/instrument/note/tempo.h"

namespace reasampler::instrument::note {

// The ladder and the offsets both feed the tempo conversions, so both must sit inside the
// domain fromBpm validates — checked here because this is the one file that composes them.
static_assert(kMaxDivisionBeats <= kMaxConvertibleMagnitude,
              "the note-length ladder must stay inside the tempo conversions' domain");

class Velocity {
public:
    static constexpr int kMin = 1;  // 0 is note-off in MIDI; a programmed note must sound
    static constexpr int kMax = 127;

    Velocity() = default;
    static Velocity of(int value);  // clamped into [kMin, kMax]

    constexpr std::uint8_t value() const { return value_; }

private:
    std::uint8_t value_ = 100;
};

bool operator==(Velocity a, Velocity b);

enum class Denomination : std::uint8_t { Milliseconds, Beats };

class OffsetAmount;

// The one door. Normalizes both fields so nothing downstream has to: a magnitude past
// +/-kMaxConvertibleMagnitude clamps to it, a NaN magnitude — which names no value to clamp
// toward — becomes zero, and a denomination outside the enum becomes Milliseconds, the
// field's own default. A corrupt persisted record therefore resolves to a plausible offset
// rather than an unrepresentable one, and no two readers can disagree about which.
OffsetAmount offsetOf(double magnitude, Denomination denomination);
OffsetAmount offsetFromMs(double ms);
OffsetAmount offsetFromBeats(double beats);

// One magnitude, in the denomination it was entered in; the other view is derived on demand
// and never stored. Which one was entered is itself the intent: a beats offset must follow a
// tempo change and a ms offset must hold still, and only a stored denomination says which.
class OffsetAmount {
public:
    OffsetAmount() = default;

    constexpr double magnitude() const { return magnitude_; }
    constexpr Denomination denomination() const { return denomination_; }

private:
    OffsetAmount(double magnitude, Denomination denomination)
        : magnitude_(magnitude), denomination_(denomination) {}
    friend OffsetAmount offsetOf(double magnitude, Denomination denomination);

    double magnitude_ = 0.0;
    Denomination denomination_ = Denomination::Milliseconds;
};

static_assert(!std::is_constructible_v<OffsetAmount, double, Denomination>,
              "offsetOf must be the only way to give an OffsetAmount a value");

bool operator==(OffsetAmount a, OffsetAmount b);
bool operator!=(OffsetAmount a, OffsetAmount b);

double offsetMs(OffsetAmount amount, Tempo tempo);
double offsetBeats(OffsetAmount amount, Tempo tempo);
double offsetSeconds(OffsetAmount amount, Tempo tempo);

// The unit toggle: the same instant restated in the other denomination.
OffsetAmount redenominate(OffsetAmount amount, Denomination to, Tempo tempo);

// Edit the magnitude via its non-stored view without changing which denomination is stored
// — a popup's ms and beats fields both stay live no matter which one the offset was entered
// in; only `redenominate` changes the stored denomination itself.
OffsetAmount withMsView(OffsetAmount amount, double ms, Tempo tempo);
OffsetAmount withBeatsView(OffsetAmount amount, double beats, Tempo tempo);

// The largest exact note length, in seconds — the ms ceiling restated in the unit an exact
// length is entered in, so a length and an offset cap at the same instant.
inline constexpr double kMaxLengthSeconds = msToSeconds(kMaxConvertibleMagnitude);

class NoteLength;

// The two doors. A DERIVED length is exact: `lengthOfSeconds` is what every computed duration
// takes, and rounding one onto the ladder is what truncates a source longer than the ladder's
// top rung. A PICKED length is a `Division`, because a picker's rungs are the point. Both
// clamp: a negative or NaN duration names no length and becomes zero, a magnitude past
// kMaxLengthSeconds clamps to it, and `makeDivision` already holds the rung's own domain.
NoteLength lengthOfDivision(Division division);
NoteLength lengthOfSeconds(double seconds);

// One length, in whichever denomination its door established. There is no accessor per
// denomination: `noteLengthSeconds` resolves both, so no reader can pick the wrong one.
class NoteLength {
public:
    NoteLength() = default;  // a quarter note — Division's own default

private:
    NoteLength(Division division, double seconds, bool exact)
        : division_(division), seconds_(seconds), exact_(exact) {}
    friend NoteLength lengthOfDivision(Division division);
    friend NoteLength lengthOfSeconds(double seconds);
    friend double noteLengthSeconds(NoteLength length, Tempo tempo);
    friend bool operator==(NoteLength a, NoteLength b);

    Division division_{};
    double seconds_ = 0.0;
    bool exact_ = false;
};

static_assert(!std::is_constructible_v<NoteLength, Division>,
              "lengthOfDivision must be the only way to give a NoteLength a rung");

bool operator==(NoteLength a, NoteLength b);
bool operator!=(NoteLength a, NoteLength b);

double noteLengthSeconds(NoteLength length, Tempo tempo);

// Two types rather than one carrying an anchor field: the anchor is then unswappable at
// compile time. Sign is uniform — positive is later in time — so a capture that opens before
// the note is a negative start offset, and a negative end offset truncates before release.
class StartOffset {
public:
    StartOffset() = default;
    explicit StartOffset(OffsetAmount amount) : amount_(amount) {}
    OffsetAmount amount() const { return amount_; }

private:
    OffsetAmount amount_{};
};

class EndOffset {
public:
    EndOffset() = default;
    explicit EndOffset(OffsetAmount amount) : amount_(amount) {}
    OffsetAmount amount() const { return amount_; }

private:
    OffsetAmount amount_{};
};

static_assert(!std::is_constructible_v<StartOffset, EndOffset>,
              "StartOffset and EndOffset must not be interchangeable at compile time");
static_assert(!std::is_convertible_v<OffsetAmount, StartOffset>,
              "the anchor constructor must stay explicit");

struct NoteProgram {
    NoteLength length{};
    StartOffset start{};
    EndOffset end{};
    Velocity velocity{};
};

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

struct ResolvedNote {
    double noteOffSeconds = 0.0;       // == the note's sounding length, note-on being 0
    double captureStartSeconds = 0.0;  // negative when the capture opens before the note
    double captureEndSeconds = 0.0;
    std::uint8_t velocity = Velocity{}.value();  // resolveNote always overwrites this
    // True when the programmed end offset inverted the window and resolveNote collapsed it
    // to zero length instead — lets a popup explain an empty window rather than just show one.
    bool windowCollapsed = false;

    double captureLengthSeconds() const { return captureEndSeconds - captureStartSeconds; }
};

// Total: every field of the result is finite for every constructible program and tempo,
// which is why there is no failure path here. See this directory's CLAUDE.md.
ResolvedNote resolveNote(const NoteProgram& program, Tempo tempo);

}  // namespace reasampler::instrument::note
