// musical_division — the note-length ladder the capture signal is programmed from: 1/64
// through 64/1, each straight, dotted, or triplet. Lengths are in BEATS only; the tempo
// resolution belongs to `tempo`, which keeps this ladder provable without one.

#pragma once

#include <cstdint>
#include <string>
#include <type_traits>

namespace reasampler::instrument::note {

enum class DivisionModifier : std::uint8_t {
    Straight,
    Dotted,   // x 3/2
    Triplet,  // x 2/3
};

// A rung of the ladder is the base-2 exponent of its length in quarter notes: -4 is 1/64,
// 0 is 1/4, 2 is 1/1, 8 is 64/1. Holding the exponent rather than a table of literal beat
// counts keeps every straight and dotted length exactly representable in double.
inline constexpr int kMinQuarterExponent = -4;
inline constexpr int kMaxQuarterExponent = 8;
inline constexpr int kRungCount = kMaxQuarterExponent - kMinQuarterExponent + 1;
inline constexpr int kModifierCount = 3;
inline constexpr int kDivisionCount = kRungCount * kModifierCount;

// The longest programmable note — the dotted top rung — so a caller composing this ladder
// with the tempo conversions can check the two domains against each other at compile time.
inline constexpr double kMaxDivisionBeats = (1 << kMaxQuarterExponent) * 1.5;

class Division;

// Off-ladder inputs clamp rather than reject: the only ways to reach one are a corrupt
// persisted record or a picker bug, and the nearest legal length beats a nonsense duration.
// An unnamed modifier byte has no nearest rung to fall to, so it takes the field's default.
Division makeDivision(int quarterExponent, DivisionModifier modifier);

// In-domain by construction — `makeDivision` is the only door and it clamps BOTH fields, so
// every reader below trusts the stored pair instead of re-clamping it, and equality compares
// the two fields raw without disagreeing with any of them.
class Division {
public:
    Division() = default;  // 1/4 straight

    constexpr std::int8_t quarterExponent() const { return quarterExponent_; }
    constexpr DivisionModifier modifier() const { return modifier_; }

private:
    Division(std::int8_t quarterExponent, DivisionModifier modifier)
        : quarterExponent_(quarterExponent), modifier_(modifier) {}
    friend Division makeDivision(int quarterExponent, DivisionModifier modifier);

    std::int8_t quarterExponent_ = 0;
    DivisionModifier modifier_ = DivisionModifier::Straight;
};

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

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

// Length in beats (quarter notes). Always > 0, and never above kMaxDivisionBeats.
double divisionBeats(Division d);

// Picker order: shortest rung first, straight/dotted/triplet within each rung. Index is
// presentation order only — see this directory's CLAUDE.md before persisting one.
Division divisionAt(int index);
int divisionIndex(Division d);

// The notation divisions are named in: "1/16", "1/8.", "1/4t", "4/1".
std::string divisionLabel(Division d);

}  // namespace reasampler::instrument::note
