// filter_morph.h — the continuous morph: normalized position to tap weights under one of two
// laws, and the fold of those weights into the three multipliers the kernel actually applies.
// An SVF produces all three taps from one state, so the morph is a blend, never a coefficient
// switch. Weights are computed at prepare() cadence; the law never reaches the per-sample path.

#pragma once

namespace reasampler::instrument::engine::filter {

// Which shape the sweep traces between its two fixed endpoints. This selects CHARACTER, not
// topology — same SVF, same coefficients, same kernel under either law; only the centre differs.
//
// HighBandLow is enumerator 0 deliberately: a zero-initialized or absent persisted field then
// lands on the default rather than on the SEM leg.
enum class MorphLaw {
    // HP -> BP -> LP. Crossfades ADJACENT taps only, so the corner magnitude is flat at Q the
    // whole way across. The default.
    HighBandLow,
    // HP -> notch -> LP, the Oberheim SEM. One crossfade weighting HP and LP together, bp == 0
    // throughout; the notch falls out of the antiphase cancellation rather than being tuned in.
    HighNotchLow,
};

// Weight on each SVF tap. Under HighBandLow exactly one of hp/lp is nonzero at a time — that law
// crossfades adjacent taps only, never HP against LP. Under HighNotchLow bp is always zero and
// hp/lp carry weight together, which is precisely what cuts the notch.
struct MorphWeights {
    float hp = 0.0f;
    float bp = 0.0f;
    float lp = 1.0f;
};

// HP at 0.0, LP at 1.0 under BOTH laws; the centre is a band-pass under HighBandLow and a notch
// under HighNotchLow. Out-of-range norm clamps to the endpoints; NaN clamps to neither (every
// comparison against it is false) and lands on the law's degenerate — pure band-pass under
// HighBandLow, pure high-pass under HighNotchLow, which has no band tap to land on.
//
// Equal-power (cos/sin) in both laws rather than linear, and that choice is forced by the
// topology rather than picked by ear. At the corner frequency the three taps are HP = jQ,
// BP = Q, LP = -jQ, so ADJACENT taps are in exact QUADRATURE there (and the bilinear transform
// preserves that exactly at the prewarped corner). Under HighBandLow's cos/sin pair the corner
// magnitude is therefore Q*sqrt(cos^2 + sin^2) = Q at every morph position — algebraically flat
// across the whole sweep. A linear crossfade of the same quadrature pair would sag to Q/sqrt(2),
// a 3 dB hole mid-leg.
//
// HP and LP are exactly ANTIPHASE at the corner (+90 and -90 degrees), so a law giving both
// simultaneous weight cancels there. HighBandLow avoids that by staying adjacent; HighNotchLow
// uses it — one equal-power crossfade of HP against LP over the whole sweep puts equal weights
// at the centre and the null is exact by construction, not tuned. That is why the corner-flat-at-Q
// guarantee is specific to HighBandLow: on the SEM leg the corner magnitude deliberately goes to
// zero at the centre. Equal power still holds off the notch frequency, so neither law's legs sag.
MorphWeights morphWeights(float norm, MorphLaw law);

// The kernel applies out = m0*v0 + m1*v1 + m2*v2, where v0 is the input and v1/v2 are the SVF's
// band and low outputs. Folding hp = v0 - k*v1 - v2 into the weights here keeps the per-sample
// path at three multiplies and spares it ever forming the high tap.
struct MorphMix {
    float m0 = 0.0f;
    float m1 = 0.0f;
    float m2 = 1.0f;
};

MorphMix morphMix(const MorphWeights& w, float k);

// Passes the input through untouched, whatever the morph position asks for. Reserved for a
// sample rate we cannot form a filter from: silencing an instrument is a worse failure than
// ignoring the morph, and at g == 0 a low-pass tap is analytically silent.
MorphMix bypassMix();

}  // namespace reasampler::instrument::engine::filter
