#pragma once

namespace chowdsp::ResamplingTypes
{
/** A resampling algorithm using Lanczos interpolation.
 *  This implementation is inspired by the one done by
 *  @baconpaul for the Surge Synthesizer.
 * 
 *  `BUFFER_SIZE` determines how large of a table the resampler
 *  should use for storing the Lanczos kernel.
 * 
 *  `A` determines the width of the kernel window (in samples),
 *  and should ALWAYS be a multiple of 4.
 *  
 *  Reference: https://en.wikipedia.org/wiki/Lanczos_resampling
 */
template <size_t BUFFER_SIZE = 4096, size_t A = 4>
class LanczosResampler : public BaseResampler
{
public:
    /** Default constructor */
    LanczosResampler()
    {
        if (tablesInitialized)
            return;

        for (size_t t = 0; t < tableObs; ++t)
        {
            double x0 = dx * (double) t;
            for (size_t i = 0; i < filterWidth; ++i)
            {
                double x = x0 + (double) i - A;
                lanczosTable[t][i] = (float) kernel (x);
            }
        }
        for (size_t t = 0; t < tableObs; ++t)
        {
            for (size_t i = 0; i < filterWidth; ++i)
            {
                lanczosTableDX[t][i] =
                    lanczosTable[(t + 1) & (tableObs - 1)][i] - lanczosTable[t][i];
            }
        }

        // wrap case
        std::copy (lanczosTable[0], &lanczosTable[0][filterWidth], lanczosTable[tableObs]);
        std::copy (lanczosTableDX[0], &lanczosTableDX[0][filterWidth], lanczosTableDX[tableObs]);

        tablesInitialized = true;
    }

    /** Prepares the resampler for a given state sample rate */
    void prepare (double /*sampleRate*/, double startRatio = 1.0) override
    {
        ratio = startRatio;
        dPhaseI = 1.0;
        dPhaseO = 1.0 / ratio;

        reset();
    }

    /** Resets the state of the resampler */
    void reset() override
    {
        snapOutToIn();
        std::fill (state, &state[BUFFER_SIZE * 2], 0.0f);
    }

    /** Sets the ratio of the output sample rate over input sample rate */
    void setResampleRatio (float newRatio) override
    {
        ratio = static_cast<double> (newRatio);
        dPhaseO = 1.0 / ratio;
    }

    /** Return the ratio of the output sample rate over input sample rate */
    [[nodiscard]] float getResampleRatio() const noexcept override { return (float) ratio; }

    /** Processes a buffer of samples
         * 
         *  @return the number of output samples generated by the resampler
         */
    size_t process (const float* input, float* output, size_t numSamples) noexcept override
    {
        // If the number of input samples is too large, then we need to process in pieces to avoid buffer overrun
        if (numSamples > BUFFER_SIZE / 2)
        {
            size_t samplesGenerated = 0;
            for (size_t samplesProcessed = 0; samplesProcessed < numSamples;)
            {
                auto samplesToProcess = juce::jmin (numSamples - samplesProcessed, BUFFER_SIZE / 2);
                samplesGenerated += process (input + samplesProcessed, output + samplesGenerated, samplesToProcess);
                samplesProcessed += samplesToProcess;
            }

            return samplesGenerated;
        }

        renormalizePhases();

        for (size_t i = 0; i < numSamples; ++i)
            push (input[i]);

        return populateNext (output, size_t ((double) numSamples * ratio) + 1);
    }

private:
    static constexpr size_t filterWidth = A * 2;
    static constexpr size_t tableObs = BUFFER_SIZE * 2;
    static constexpr double dx = 1.0 / (tableObs);

    static float lanczosTable alignas (16)[tableObs + 1][filterWidth], lanczosTableDX alignas (16)[tableObs + 1][filterWidth];
    static bool tablesInitialized;

    float state[BUFFER_SIZE * 2] { 0.0f };
    int wp = 0;
    double phaseI = 0.0, phaseO = 0.0, dPhaseI = 0.0, dPhaseO = 0.0;
    double ratio = 1.0;

    inline double kernel (double x)
    {
        if (fabs (x) < 1e-7)
            return 1;

        constexpr auto pi = juce::MathConstants<double>::pi;
        return A * std::sin (pi * x) * std::sin (pi * x / A) / (pi * pi * x * x);
    }

    inline void push (float f)
    {
        state[wp] = f;
        state[wp + (int) BUFFER_SIZE] = f; // this way we can always wrap
        wp = (wp + 1) & ((int) BUFFER_SIZE - 1);
        phaseI += dPhaseI;
    }

    [[nodiscard]] inline float readZOH (double xBack) const
    {
        double p0 = wp - xBack;
        int idx0 = (int) p0;
        idx0 = (idx0 + (int) BUFFER_SIZE) & ((int) BUFFER_SIZE - 1);
        if (idx0 <= (int) A)
            idx0 += BUFFER_SIZE;

        return state[idx0];
    }

    [[nodiscard]] inline float readLin (double xBack) const
    {
        double p0 = wp - xBack;
        int idx0 = (int) p0;
        auto frac = float (p0 - idx0);
        idx0 = (idx0 + (int) BUFFER_SIZE) & ((int) BUFFER_SIZE - 1);
        if (idx0 <= (int) A)
            idx0 += BUFFER_SIZE;

        return (1.0f - frac) * state[idx0] + frac * state[idx0 + 1];
    }

    [[nodiscard]] inline float read (double xBack) const
    {
        double p0 = wp - xBack;
        int idx0 = (int) floor (p0);
        double off0 = 1.0 - (p0 - idx0);

        idx0 = (idx0 + (int) BUFFER_SIZE) & ((int) BUFFER_SIZE - 1);
        idx0 += (idx0 <= (int) A) * (int) BUFFER_SIZE;

        double off0byto = off0 * tableObs;
        int tidx = (int) off0byto;
        double fidx = (off0byto - tidx);

        using SR = juce::dsp::SIMDRegister<float>;
        auto rv = SR (0.0f);
        const auto fl = SR ((float) fidx);
        for (size_t i = 0; i < filterWidth; i += SR::size())
        {
            auto fn = SR::fromRawArray (&lanczosTable[tidx][i]);
            auto dfn = SR::fromRawArray (&lanczosTableDX[tidx][i]);
            fn = fn + (dfn * fl);

            auto dn = SIMDUtils::loadUnaligned (&state[idx0 - (int) A + (int) i]);
            rv += fn * dn;
        }

        return rv.sum();
    }

    size_t populateNext (float* f, size_t max)
    {
        size_t populated = 0;
        while (populated < max && (phaseI - phaseO) > A + 1)
        {
            f[populated] = read (phaseI - phaseO);
            phaseO += dPhaseO;
            populated++;
        }
        return populated;
    }

    inline void advanceReadPointer (size_t n) { phaseO += (double) n * dPhaseO; }
    inline void snapOutToIn()
    {
        phaseO = 0;
        phaseI = 0;
    }

    inline void renormalizePhases()
    {
        phaseI -= phaseO;
        phaseO = 0;
    }
};

template <size_t BUFFER_SIZE, size_t A>
float LanczosResampler<BUFFER_SIZE, A>::lanczosTable alignas (16)[LanczosResampler::tableObs + 1][LanczosResampler::filterWidth];

template <size_t BUFFER_SIZE, size_t A>
float LanczosResampler<BUFFER_SIZE, A>::lanczosTableDX alignas (16)[LanczosResampler::tableObs + 1][LanczosResampler::filterWidth];

template <size_t BUFFER_SIZE, size_t A>
bool LanczosResampler<BUFFER_SIZE, A>::tablesInitialized = false;

} // namespace chowdsp::ResamplingTypes
