// param_units.cpp — see param_units.h. Every law here is CALLED, never restated: the stage-time
// and semitone tapers are param_taper's, the curve travel is curve_law's, the filter's four are
// filter_params' own frozen laws, and the dB sweep is master_gain's.

#include "core/instrument/param/param_units.h"

#include "core/instrument/engine/filter/filter_params.h"
#include "core/instrument/engine/master_gain.h"
#include "core/instrument/map/play_seconds.h"
#include "core/instrument/ui/deck_values.h"
#include "core/instrument/ui/param_taper.h"
#include "core/util/clamp01.h"
#include "core/util/curve_law.h"

namespace reasampler::instrument::param {

namespace {

using map::PlaySeconds;
using util::clamp01;

// A whole displayed percent is a different plain full scale per category; these are the two
// non-100 ones, named rather than inlined so the range table and the maps cannot disagree.
constexpr double kPercentFullScale = 100.0;
const double kKeyTrackFullScale = kPercentFullScale * ui::kKeyTrackMax;  // 0..200 %

}  // namespace

UnitKind unitKindFor(DeckParam deck) {
    switch (deck) {
        case DeckParam::kAttack:
        case DeckParam::kHold:
        case DeckParam::kDecay:
        case DeckParam::kRelease:
        case DeckParam::kTrigAttack:
        case DeckParam::kTrigDecay:
        case DeckParam::kPitchEnvAttack:
        case DeckParam::kPitchEnvDecay:
        case DeckParam::kFilterEnvAttack:
        case DeckParam::kFilterEnvHold:
        case DeckParam::kFilterEnvDecay:
        case DeckParam::kFilterEnvRelease:
        case DeckParam::kFilterTrigAttack:
        case DeckParam::kFilterTrigDecay:
            return UnitKind::Time;
        case DeckParam::kPitch:
        case DeckParam::kPitchEnvDepth:
            return UnitKind::Semitones;
        case DeckParam::kSustain:
        case DeckParam::kTrigLength:
        case DeckParam::kTrigHold:
        case DeckParam::kPitchEnvHold:
        case DeckParam::kFilterEnvSustain:
        case DeckParam::kFilterTrigHold:
        case DeckParam::kFilterMorph:
            return UnitKind::PercentUnipolar;
        case DeckParam::kKeyTrack:
        case DeckParam::kFilterKeyTrack:
            return UnitKind::PercentKeyTrack;
        case DeckParam::kFilterModAmt:
        case DeckParam::kFilterVel:
            return UnitKind::PercentBipolar;
        case DeckParam::kRate:
            return UnitKind::PercentRate;
        case DeckParam::kMasterGain:
            return UnitKind::Decibels;
        case DeckParam::kFilterCutoff:
            return UnitKind::Hertz;
        // The twelve curve exponents and the filter's two dimensionless tone controls. Listed
        // rather than defaulted, and everything with no parameter row at all is listed with
        // them: a `default:` here would let a control promoted later inherit Dimensionless
        // silently, and an exposed parameter's normalization is frozen on the first shipped
        // build — so the wrong answer would be permanent rather than correctable. THIS switch is
        // the one that has to be exhaustive; the `default:` arms further down are pre-dispatch
        // filters that fall through to it, so they inherit its exhaustiveness rather than
        // needing their own.
        case DeckParam::kFilterQ:
        case DeckParam::kFilterDrive:
        case DeckParam::kAttackCurve:
        case DeckParam::kDecayCurve:
        case DeckParam::kReleaseCurve:
        case DeckParam::kTrigAttackCurve:
        case DeckParam::kTrigDecayCurve:
        case DeckParam::kPitchEnvAttackCurve:
        case DeckParam::kPitchEnvDecayCurve:
        case DeckParam::kFilterEnvAttackCurve:
        case DeckParam::kFilterEnvDecayCurve:
        case DeckParam::kFilterEnvReleaseCurve:
        case DeckParam::kFilterTrigAttackCurve:
        case DeckParam::kFilterTrigDecayCurve:
        case DeckParam::kPlayMode:
        case DeckParam::kPitchEngine:
        case DeckParam::kPitchEnvEnable:
        case DeckParam::kFilterEnable:
        case DeckParam::kFilterLaw:
        case DeckParam::kAmpVelCurve:
        case DeckParam::kPitchVelCurve:
        case DeckParam::kFilterVelCurve:
        case DeckParam::kAmpEnvSelect:
        case DeckParam::kPitchEnvSelect:
        case DeckParam::kFilterEnvSelect:
        case DeckParam::kAmpEnvMode:
        case DeckParam::kPitchEnvMode:
        case DeckParam::kFilterEnvMode:
        case DeckParam::kVoiceCount:
        case DeckParam::kVoiceMode:
        case DeckParam::kMonoTrigger:
        case DeckParam::kLimiterEnable:
        case DeckParam::kMasterMeter:
        case DeckParam::kMasterGr:
            return UnitKind::Dimensionless;
        // The sentinel, on its own arm: it names no control, so its unit string, plain range and
        // toPlain law are all arbitrary. It is here only because the switch is exhaustive, and
        // it stays out of the run above so that run reads as a list of real controls.
        case DeckParam::kCount:
            return UnitKind::Dimensionless;
    }
    return UnitKind::Dimensionless;  // unreachable for a valid enumerator; silences a warning.
}

const char* unitStringFor(DeckParam deck) {
    switch (unitKindFor(deck)) {
        case UnitKind::Time:            return "ms";
        case UnitKind::Semitones:       return "st";
        case UnitKind::PercentUnipolar:
        case UnitKind::PercentKeyTrack:
        case UnitKind::PercentBipolar:
        case UnitKind::PercentRate:     return "%";
        case UnitKind::Decibels:        return "dB";
        case UnitKind::Hertz:           return "Hz";
        case UnitKind::Dimensionless:   return "";
    }
    return "";
}

PlainRange plainRangeFor(DeckParam deck) {
    switch (deck) {
        case DeckParam::kFilterQ:
            return {static_cast<double>(engine::filter::kFilterQMin),
                    static_cast<double>(engine::filter::kFilterQMax)};
        case DeckParam::kFilterDrive:
            return {0.0, static_cast<double>(engine::filter::kFilterDriveDepthMax)};
        default:
            break;
    }
    switch (unitKindFor(deck)) {
        case UnitKind::Time:            return {0.0, ui::kEnvTimeMaxSeconds * 1000.0};
        case UnitKind::Semitones:       return {-ui::kPitchDepthMaxSemis, ui::kPitchDepthMaxSemis};
        case UnitKind::PercentUnipolar: return {0.0, kPercentFullScale};
        case UnitKind::PercentKeyTrack: return {0.0, kKeyTrackFullScale};
        case UnitKind::PercentBipolar:  return {-kPercentFullScale, kPercentFullScale};
        case UnitKind::PercentRate:     return {ui::kRateMinRatio * kPercentFullScale,
                                                ui::kRateMaxRatio * kPercentFullScale};
        case UnitKind::Decibels:        return {engine::kMasterGainMinDb, engine::kMasterGainMaxDb};
        case UnitKind::Hertz:           return {static_cast<double>(engine::filter::kFilterCutoffMinHz),
                                                static_cast<double>(engine::filter::kFilterCutoffMaxHz)};
        case UnitKind::Dimensionless:   return {util::kCurveMin, util::kCurveMax};
    }
    return {};
}

double toPlain(DeckParam deck, double normalized) {
    switch (deck) {
        case DeckParam::kFilterCutoff:
            return static_cast<double>(
                engine::filter::filterCutoffHzFromNorm(static_cast<float>(normalized)));
        case DeckParam::kFilterQ:
            return static_cast<double>(
                engine::filter::filterQFromNorm(static_cast<float>(normalized)));
        case DeckParam::kFilterDrive:
            return static_cast<double>(
                engine::filter::filterDriveDepthFromNorm(static_cast<float>(normalized)));
        case DeckParam::kMasterGain:
            // -inf at norm 0 — true silence, and the one plain value outside the declared range.
            return engine::masterGainDbFromNorm(normalized);
        default:
            break;
    }
    switch (unitKindFor(deck)) {
        case UnitKind::Time:
            return ui::timeSecondsFromNorm(normalized) * 1000.0;
        case UnitKind::Semitones:
            return ui::depthSemitonesFromNorm(normalized, ui::kPitchDepthMaxSemis);
        case UnitKind::PercentUnipolar:
            return clamp01(normalized) * kPercentFullScale;
        case UnitKind::PercentKeyTrack:
            return clamp01(normalized) * kKeyTrackFullScale;
        case UnitKind::PercentBipolar:
            return ui::deckBipolarFromNorm(normalized) * kPercentFullScale;
        case UnitKind::PercentRate:
            return ui::rateRatioFromNorm(normalized, ui::kRateMinRatio, ui::kRateMaxRatio) *
                   kPercentFullScale;
        case UnitKind::Dimensionless:
            // Undetented: a host-facing continuous range stays continuous (Daniel, 2026-08-02) —
            // the detent is a drag affordance, not part of the value law. See hostStoredFromNorm.
            return util::curveFromKnobNormUndetented(normalized);
        case UnitKind::Decibels:
        case UnitKind::Hertz:
            break;  // handled above
    }
    return normalized;
}

double toNormalized(DeckParam deck, double plain) {
    switch (deck) {
        case DeckParam::kFilterCutoff:
            return static_cast<double>(
                engine::filter::filterNormFromCutoffHz(static_cast<float>(plain)));
        case DeckParam::kFilterQ:
            return static_cast<double>(
                engine::filter::filterNormFromQ(static_cast<float>(plain)));
        case DeckParam::kFilterDrive:
            return static_cast<double>(
                engine::filter::filterNormFromDriveDepth(static_cast<float>(plain)));
        case DeckParam::kMasterGain:
            return engine::masterGainNormFromDb(plain);
        default:
            break;
    }
    switch (unitKindFor(deck)) {
        case UnitKind::Time:
            return ui::timeNormFromSeconds(plain / 1000.0);
        case UnitKind::Semitones:
            return ui::depthNormFromSemitones(plain, ui::kPitchDepthMaxSemis);
        case UnitKind::PercentUnipolar:
            return clamp01(plain / kPercentFullScale);
        case UnitKind::PercentKeyTrack:
            return clamp01(plain / kKeyTrackFullScale);
        case UnitKind::PercentBipolar:
            return ui::deckNormFromBipolar(plain / kPercentFullScale);
        case UnitKind::PercentRate:
            return ui::rateNormFromRatio(plain / kPercentFullScale, ui::kRateMinRatio,
                                         ui::kRateMaxRatio);
        case UnitKind::Dimensionless:
            return util::knobNormFromCurve(plain);
        case UnitKind::Decibels:
        case UnitKind::Hertz:
            break;  // handled above
    }
    return plain;
}

double hostStoredFromNorm(DeckParam deck, double normalized) {
    // See the header for why the detent is a drag affordance and not part of the value law.
    if (ui::deckParamUnit(deck) == ui::UnitCategory::Exponent) {
        return util::curveFromKnobNormUndetented(normalized);
    }
    return ui::storedFromNorm(deck, normalized);
}

ValueHome valueHomeFor(DeckParam deck) {
    PlaySeconds defaults;
    if (ui::deckFloatField(deck, defaults)) return ValueHome::ParamSetNorm;
    if (ui::deckDoubleField(deck, defaults)) return ValueHome::ParamSet;
    if (deck == DeckParam::kMasterGain || deck == DeckParam::kKeyTrack) {
        return ValueHome::InstanceScalar;
    }
    return ValueHome::None;
}

bool storesNormalized(DeckParam deck) {
    return valueHomeFor(deck) == ValueHome::ParamSetNorm;
}

double defaultPlain(DeckParam deck) {
    PlaySeconds defaults;
    // The filter's four store the normalized position itself, so their plain default is that
    // stored position read THROUGH the law — the law is the display, never the storage.
    if (const float* stored = ui::deckFloatField(deck, defaults)) {
        return toPlain(deck, static_cast<double>(*stored));
    }
    // The two instance scalars, whose default is not a field of PlaySeconds.
    if (deck == DeckParam::kMasterGain) return 0.0;  // unity, and the sharpest exactness case
    if (deck == DeckParam::kKeyTrack) {
        return kKeyTrackDefault * kPercentFullScale;
    }
    const double* field = ui::deckDoubleField(deck, defaults);
    if (!field) return 0.0;
    switch (unitKindFor(deck)) {
        // Time converts seconds -> ms here and ms -> seconds in toNormalized, so its exactness
        // additionally rests on x*1000/1000 == x — param_taper guarantees its quantum in SECONDS,
        // not in ms. It holds for today's three Time defaults; a new one is a case to re-check.
        case UnitKind::Time:            return *field * 1000.0;  // stored seconds
        case UnitKind::PercentUnipolar: return *field * kPercentFullScale;
        case UnitKind::PercentKeyTrack: return *field * kPercentFullScale;  // stored 0..2
        case UnitKind::PercentBipolar:  return *field * kPercentFullScale;
        case UnitKind::PercentRate:     return *field * kPercentFullScale;  // stored ratio
        case UnitKind::Semitones:
        case UnitKind::Dimensionless:   return *field;  // already the plain unit
        case UnitKind::Decibels:
        case UnitKind::Hertz:           break;          // handled above
    }
    return *field;
}

double defaultNormalized(DeckParam deck) {
    PlaySeconds defaults;
    if (const float* stored = ui::deckFloatField(deck, defaults)) {
        return static_cast<double>(*stored);  // verbatim: no taper on the reset path
    }
    return toNormalized(deck, defaultPlain(deck));
}

}  // namespace reasampler::instrument::param
