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

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

#include <algorithm>
#include <cmath>

#include "core/instrument/engine/filter/filter_morph.h"   // MorphLaw (the law toggle's value)
#include "core/instrument/engine/master_gain.h"           // the dB taper the whole-dB snap reads
#include "core/util/clamp01.h"
#include "core/util/curve_law.h"                          // the ONE curve-exponent domain

namespace reasampler::instrument::ui {

using engine::filter::MorphLaw;
using util::clamp01;

double deckParamNorm(DeckParam id, const PlaySeconds& play) {
    switch (id) {
        case DeckParam::kPlayMode:      return play.playMode == PlayMode::Trigger ? 1.0 : 0.0;
        case DeckParam::kAmpEnvMode:    return play.ampSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
        case DeckParam::kPitchEnvMode:  return play.pitchSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
        case DeckParam::kFilterEnvMode: return play.filterSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
        case DeckParam::kPitchEngine:   return play.pitchEngine == PitchEngine::Preserve ? 1.0 : 0.0;
        case DeckParam::kRate:
            return rateNormFromRatio(play.playRate, kRateMinRatio, kRateMaxRatio);
        case DeckParam::kPitch:
            return depthNormFromSemitones(play.pitchOffsetSemitones, kPitchDepthMaxSemis);
        case DeckParam::kAttack:        return timeNormFromSeconds(play.adsr.attackSeconds);
        case DeckParam::kHold:          return timeNormFromSeconds(play.adsr.holdSeconds);
        case DeckParam::kDecay:         return timeNormFromSeconds(play.adsr.decaySeconds);
        case DeckParam::kSustain:       return clamp01(play.adsr.sustainLevel);
        case DeckParam::kRelease:       return timeNormFromSeconds(play.adsr.releaseSeconds);
        case DeckParam::kAttackCurve:   return util::knobNormFromCurve(play.adsr.attackCurve);
        case DeckParam::kDecayCurve:    return util::knobNormFromCurve(play.adsr.decayCurve);
        case DeckParam::kReleaseCurve:  return util::knobNormFromCurve(play.adsr.releaseCurve);
        case DeckParam::kTrigLength:    return clamp01(play.trigger.lengthFraction);
        case DeckParam::kTrigAttack:    return timeNormFromSeconds(play.trigAhd.attackSeconds);
        case DeckParam::kTrigHold:      return clamp01(play.trigAhd.holdFraction);
        case DeckParam::kTrigDecay:     return timeNormFromSeconds(play.trigAhd.decaySeconds);
        case DeckParam::kTrigAttackCurve: return util::knobNormFromCurve(play.trigAhd.attackCurve);
        case DeckParam::kTrigDecayCurve:  return util::knobNormFromCurve(play.trigAhd.decayCurve);
        case DeckParam::kPitchEnvEnable: return play.pitchEnv.enabled ? 1.0 : 0.0;
        case DeckParam::kPitchEnvAttack:
            return timeNormFromSeconds(play.pitchEnv.shape.attackSeconds);
        case DeckParam::kPitchEnvHold:   return clamp01(play.pitchEnv.shape.holdFraction);
        case DeckParam::kPitchEnvDecay:
            return timeNormFromSeconds(play.pitchEnv.shape.decaySeconds);
        case DeckParam::kPitchEnvAttackCurve:
            return util::knobNormFromCurve(play.pitchEnv.shape.attackCurve);
        case DeckParam::kPitchEnvDecayCurve:
            return util::knobNormFromCurve(play.pitchEnv.shape.decayCurve);
        case DeckParam::kPitchEnvDepth:
            return depthNormFromSemitones(play.pitchEnv.peakSemitones, kPitchDepthMaxSemis);
        // Filter. The four tone controls ARE the module's normalized positions — stored and
        // shown as-is, so the knob travel is exactly filter_params' own law.
        case DeckParam::kFilterEnable: return play.filter.enabled ? 1.0 : 0.0;
        case DeckParam::kFilterLaw:
            return play.filter.settings.morphLaw == MorphLaw::HighNotchLow ? 1.0 : 0.0;
        case DeckParam::kFilterMorph:   return clamp01(play.filter.settings.morphNorm);
        case DeckParam::kFilterCutoff:  return clamp01(play.filter.settings.cutoffNorm);
        case DeckParam::kFilterQ:       return clamp01(play.filter.settings.resonanceNorm);
        case DeckParam::kFilterDrive:   return clamp01(play.filter.settings.driveNorm);
        case DeckParam::kFilterModAmt:  return deckNormFromBipolar(play.filter.modAmount);
        case DeckParam::kFilterVel:     return deckNormFromBipolar(play.filter.velAmount);
        case DeckParam::kFilterKeyTrack: return keyTrackNormFrom(play.filter.keyTrack);
        case DeckParam::kFilterEnvAttack:  return timeNormFromSeconds(play.filter.env.attackSeconds);
        case DeckParam::kFilterEnvHold:    return timeNormFromSeconds(play.filter.env.holdSeconds);
        case DeckParam::kFilterEnvDecay:   return timeNormFromSeconds(play.filter.env.decaySeconds);
        case DeckParam::kFilterEnvSustain: return clamp01(play.filter.env.sustainLevel);
        case DeckParam::kFilterEnvRelease:
            return timeNormFromSeconds(play.filter.env.releaseSeconds);
        case DeckParam::kFilterEnvAttackCurve:
            return util::knobNormFromCurve(play.filter.env.attackCurve);
        case DeckParam::kFilterEnvDecayCurve:
            return util::knobNormFromCurve(play.filter.env.decayCurve);
        case DeckParam::kFilterEnvReleaseCurve:
            return util::knobNormFromCurve(play.filter.env.releaseCurve);
        case DeckParam::kFilterTrigAttack:
            return timeNormFromSeconds(play.filter.trigEnv.attackSeconds);
        case DeckParam::kFilterTrigHold:   return clamp01(play.filter.trigEnv.holdFraction);
        case DeckParam::kFilterTrigDecay:
            return timeNormFromSeconds(play.filter.trigEnv.decaySeconds);
        case DeckParam::kFilterTrigAttackCurve:
            return util::knobNormFromCurve(play.filter.trigEnv.attackCurve);
        case DeckParam::kFilterTrigDecayCurve:
            return util::knobNormFromCurve(play.filter.trigEnv.decayCurve);
        default: return 0.0;
    }
}

double storedFromNorm(DeckParam id, double norm) {
    switch (id) {
        // The filter's four STORE their normalized position (payload v9), so the identity IS
        // their law — deckFloatField's four, and the reason it is a separate resolver.
        case DeckParam::kFilterMorph:
        case DeckParam::kFilterCutoff:
        case DeckParam::kFilterQ:
        case DeckParam::kFilterDrive:
            return clamp01(norm);
        case DeckParam::kRate:
            return rateRatioFromNorm(norm, kRateMinRatio, kRateMaxRatio);
        case DeckParam::kPitch:
        case DeckParam::kPitchEnvDepth:
            return depthSemitonesFromNorm(norm, kPitchDepthMaxSemis);
        case DeckParam::kFilterModAmt:
        case DeckParam::kFilterVel:
            return deckBipolarFromNorm(norm);
        case DeckParam::kKeyTrack:
        case DeckParam::kFilterKeyTrack:
            return keyTrackFromNorm(norm);
        case DeckParam::kTrigLength:
            // lengthFraction is (0,1]; a small floor so a zero-length trigger never plays
            // nothing.
            return (std::max)(0.01, clamp01(norm));
        default:
            break;
    }
    // The rest are decided by the display unit alone, which is what makes the fourteen stage
    // times and the twelve curve dials one line each rather than twenty-six.
    switch (deckParamUnit(id)) {
        case UnitCategory::Milliseconds: return timeSecondsFromNorm(norm);
        case UnitCategory::Exponent:     return util::curveFromKnobNorm(norm);
        case UnitCategory::Percent:      return clamp01(norm);  // hold fractions, sustain levels
        // Decibels is master gain, whose stored value is a LINEAR gain the processor owns
        // rather than a field of the parameter set; the two enums above are handled by id.
        case UnitCategory::Semitones:
        case UnitCategory::Decibels:
        case UnitCategory::None:
            break;
    }
    return norm;
}

void setDeckParam(DeckParam id, PlaySeconds& play, double value, int segment) {
    switch (id) {
        case DeckParam::kPlayMode:
            // Gate is refused while any EG is drawn — see splineActive (play_params.h). The
            // segment paints Disabled for the same reason, so the refusal is never a surprise.
            if (segment == 0 && splineActive(play)) break;
            play.playMode = (segment == 1) ? PlayMode::Trigger : PlayMode::Gate;
            break;
        // Switching TO Spline drops Gate, which the spline model has no place for. Switching
        // back does NOT restore it: the previous mode is not stored, and silently re-gating an
        // instrument the user has since heard as a one-shot is the worse surprise.
        case DeckParam::kAmpEnvMode:
        case DeckParam::kPitchEnvMode:
        case DeckParam::kFilterEnvMode: {
            const EnvMode m = (segment == 1) ? EnvMode::Spline : EnvMode::Staged;
            if (id == DeckParam::kAmpEnvMode) play.ampSpline.mode = m;
            else if (id == DeckParam::kPitchEnvMode) play.pitchSpline.mode = m;
            else play.filterSpline.mode = m;
            break;
        }
        case DeckParam::kPitchEngine:
            play.pitchEngine = (segment == 1) ? PitchEngine::Preserve : PitchEngine::Varispeed;
            break;
        case DeckParam::kPitchEnvEnable: play.pitchEnv.enabled = (segment == 1); break;
        case DeckParam::kFilterEnable: play.filter.enabled = (segment == 1); break;
        case DeckParam::kFilterLaw:
            play.filter.settings.morphLaw =
                (segment == 1) ? MorphLaw::HighNotchLow : MorphLaw::HighBandLow;
            break;
        default:
            // Every knob: the one norm -> stored law, into the one field the control names.
            // Both halves are shared with the audio thread's live patch (param/param_live), so
            // a control cannot take a different taper or land in a different field depending on
            // which surface wrote it. A toggle or a value living outside PlaySeconds resolves to
            // neither field and falls through untouched.
            if (float* f = deckFloatField(id, play)) {
                *f = static_cast<float>(storedFromNorm(id, value));
            } else if (double* d = deckDoubleField(id, play)) {
                *d = storedFromNorm(id, value);
            }
            break;
    }
    // ONE normalization point for every control that can flip splineActive — a mode toggle
    // (above) or an enable toggle (kPitchEnvEnable/kFilterEnable), whose enabling can make an
    // already-Spline pitch/filter envelope newly active. Applying it once here, rather than at
    // each site that could cause the flip, is what keeps a future such control from reopening
    // the same hole. `resolvePlay` (sample_map.cpp) is the other caller of the shared helper.
    enforceGateUnavailableWhileDrawn(play);
}

// deckParamNorm and setDeckParam carry each id's MAP — which taper, which clamp; these two carry
// only its LOCATION, which is the whole mechanism of the taper-free reset (see deck_values.h for
// why they are exposed beyond that one caller). A toggle, radio or curve cell has no reset gesture
// and resolves to null.
double* deckDoubleField(DeckParam id, PlaySeconds& p) {
    switch (id) {
        case DeckParam::kRate:                  return &p.playRate;
        case DeckParam::kPitch:                 return &p.pitchOffsetSemitones;
        case DeckParam::kAttack:                return &p.adsr.attackSeconds;
        case DeckParam::kHold:                  return &p.adsr.holdSeconds;
        case DeckParam::kDecay:                 return &p.adsr.decaySeconds;
        case DeckParam::kSustain:               return &p.adsr.sustainLevel;
        case DeckParam::kRelease:               return &p.adsr.releaseSeconds;
        case DeckParam::kAttackCurve:           return &p.adsr.attackCurve;
        case DeckParam::kDecayCurve:            return &p.adsr.decayCurve;
        case DeckParam::kReleaseCurve:          return &p.adsr.releaseCurve;
        case DeckParam::kTrigLength:            return &p.trigger.lengthFraction;
        case DeckParam::kTrigAttack:            return &p.trigAhd.attackSeconds;
        case DeckParam::kTrigHold:              return &p.trigAhd.holdFraction;
        case DeckParam::kTrigDecay:             return &p.trigAhd.decaySeconds;
        case DeckParam::kTrigAttackCurve:       return &p.trigAhd.attackCurve;
        case DeckParam::kTrigDecayCurve:        return &p.trigAhd.decayCurve;
        case DeckParam::kPitchEnvAttack:        return &p.pitchEnv.shape.attackSeconds;
        case DeckParam::kPitchEnvHold:          return &p.pitchEnv.shape.holdFraction;
        case DeckParam::kPitchEnvDecay:         return &p.pitchEnv.shape.decaySeconds;
        case DeckParam::kPitchEnvAttackCurve:   return &p.pitchEnv.shape.attackCurve;
        case DeckParam::kPitchEnvDecayCurve:    return &p.pitchEnv.shape.decayCurve;
        case DeckParam::kPitchEnvDepth:         return &p.pitchEnv.peakSemitones;
        case DeckParam::kFilterModAmt:          return &p.filter.modAmount;
        case DeckParam::kFilterVel:             return &p.filter.velAmount;
        case DeckParam::kFilterKeyTrack:        return &p.filter.keyTrack;
        case DeckParam::kFilterEnvAttack:       return &p.filter.env.attackSeconds;
        case DeckParam::kFilterEnvHold:         return &p.filter.env.holdSeconds;
        case DeckParam::kFilterEnvDecay:        return &p.filter.env.decaySeconds;
        case DeckParam::kFilterEnvSustain:      return &p.filter.env.sustainLevel;
        case DeckParam::kFilterEnvRelease:      return &p.filter.env.releaseSeconds;
        case DeckParam::kFilterEnvAttackCurve:  return &p.filter.env.attackCurve;
        case DeckParam::kFilterEnvDecayCurve:   return &p.filter.env.decayCurve;
        case DeckParam::kFilterEnvReleaseCurve: return &p.filter.env.releaseCurve;
        case DeckParam::kFilterTrigAttack:      return &p.filter.trigEnv.attackSeconds;
        case DeckParam::kFilterTrigHold:        return &p.filter.trigEnv.holdFraction;
        case DeckParam::kFilterTrigDecay:       return &p.filter.trigEnv.decaySeconds;
        case DeckParam::kFilterTrigAttackCurve: return &p.filter.trigEnv.attackCurve;
        case DeckParam::kFilterTrigDecayCurve:  return &p.filter.trigEnv.decayCurve;
        default:                                return nullptr;
    }
}

// The filter's four tone controls store their NORMALIZED position, as floats, and their law is
// wire-frozen — hence a second resolver rather than a widened first one.
float* deckFloatField(DeckParam id, PlaySeconds& p) {
    switch (id) {
        case DeckParam::kFilterMorph:  return &p.filter.settings.morphNorm;
        case DeckParam::kFilterCutoff: return &p.filter.settings.cutoffNorm;
        case DeckParam::kFilterQ:      return &p.filter.settings.resonanceNorm;
        case DeckParam::kFilterDrive:  return &p.filter.settings.driveNorm;
        default:                       return nullptr;
    }
}

void resetDeckParam(DeckParam id, PlaySeconds& play) {
    PlaySeconds defaults;
    if (double* dst = deckDoubleField(id, play)) {
        *dst = *deckDoubleField(id, defaults);
        return;
    }
    if (float* dst = deckFloatField(id, play)) *dst = *deckFloatField(id, defaults);
}

UnitCategory deckParamUnit(DeckParam id) {
    switch (id) {
        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 UnitCategory::Milliseconds;
        // Rate DISPLAYS as a percent but its unit is the semitone — that is what puts an octave
        // and a fifth under Shift, which a whole-percent snap could not reach.
        case DeckParam::kRate:
        case DeckParam::kPitch:
        case DeckParam::kPitchEnvDepth:
            return UnitCategory::Semitones;
        // The filter's four tone controls read out in Hz / Q / drive depth but snap in whole
        // percent of the normalized position they STORE — display and snap are independent axes.
        case DeckParam::kSustain:
        case DeckParam::kTrigLength:
        case DeckParam::kTrigHold:
        case DeckParam::kPitchEnvHold:
        case DeckParam::kKeyTrack:
        case DeckParam::kFilterKeyTrack:
        case DeckParam::kFilterMorph:
        case DeckParam::kFilterCutoff:
        case DeckParam::kFilterQ:
        case DeckParam::kFilterDrive:
        case DeckParam::kFilterModAmt:
        case DeckParam::kFilterVel:
        case DeckParam::kFilterEnvSustain:
        case DeckParam::kFilterTrigHold:
            return UnitCategory::Percent;
        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:
            return UnitCategory::Exponent;
        case DeckParam::kMasterGain:
            return UnitCategory::Decibels;
        default:
            // Toggles, radios, the curve-popup cells, and the already-integer voice count.
            return UnitCategory::None;
    }
}

double snapDeckParamNorm(DeckParam id, double norm) {
    switch (deckParamUnit(id)) {
        case UnitCategory::Milliseconds:
            return timeNormFromSeconds(snapSecondsToWholeMs(timeSecondsFromNorm(norm)));
        case UnitCategory::Semitones:
            // Rate's semitones live in the ratio domain, so its snap round-trips through the rate
            // taper rather than the depth one; the other two share the depth throw.
            if (id == DeckParam::kRate) {
                return rateNormFromRatio(
                    snapRateRatioToWholeSemitone(
                        rateRatioFromNorm(norm, kRateMinRatio, kRateMaxRatio)),
                    kRateMinRatio, kRateMaxRatio);
            }
            return depthNormFromSemitones(
                snapSemitonesToWhole(depthSemitonesFromNorm(norm, kPitchDepthMaxSemis)),
                kPitchDepthMaxSemis);
        case UnitCategory::Exponent:
            return util::knobNormFromCurve(snapExponentToWhole(util::curveFromKnobNorm(norm)));
        case UnitCategory::Decibels:
            return engine::masterGainNormFromDb(
                std::nearbyint(engine::masterGainDbFromNorm(norm)));
        case UnitCategory::Percent:
            switch (id) {
                case DeckParam::kFilterModAmt:
                case DeckParam::kFilterVel:
                    return deckNormFromBipolar(
                        snapFractionToWholePercent(deckBipolarFromNorm(norm)));
                case DeckParam::kKeyTrack:
                case DeckParam::kFilterKeyTrack:
                    return keyTrackNormFrom(
                        snapFractionToWholePercent(keyTrackFromNorm(norm)));
                default:
                    return clamp01(snapFractionToWholePercent(clamp01(norm)));
            }
        case UnitCategory::None:
            break;
    }
    return norm;
}

}  // namespace reasampler::instrument::ui
