/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <math.h>

#include "IntegerRatio.h"
#include "LinearResampler.h"
#include "MultiChannelResampler.h"
#include "PolyphaseResampler.h"
#include "PolyphaseResamplerMono.h"
#include "PolyphaseResamplerStereo.h"
#include "SincResampler.h"
#include "SincResamplerStereo.h"

using namespace resampler;

MultiChannelResampler::MultiChannelResampler(const MultiChannelResampler::Builder &builder)
        : mNumTaps(builder.getNumTaps())
        , mX(builder.getChannelCount() * builder.getNumTaps() * 2)
        , mSingleFrame(builder.getChannelCount())
        , mChannelCount(builder.getChannelCount())
        {
    // Reduce sample rates to the smallest ratio.
    // For example 44100/48000 would become 147/160.
    IntegerRatio ratio(builder.getInputRate(), builder.getOutputRate());
    ratio.reduce();
    mNumerator = ratio.getNumerator();
    mDenominator = ratio.getDenominator();
    mIntegerPhase = mDenominator;
}

// static factory method
MultiChannelResampler *MultiChannelResampler::make(int32_t channelCount,
                                                   int32_t inputRate,
                                                   int32_t outputRate,
                                                   Quality quality) {
    Builder builder;
    builder.setInputRate(inputRate);
    builder.setOutputRate(outputRate);
    builder.setChannelCount(channelCount);

    switch (quality) {
        case Quality::Fastest:
            builder.setNumTaps(2);
            break;
        case Quality::Low:
            builder.setNumTaps(4);
            break;
        case Quality::Medium:
        default:
            builder.setNumTaps(8);
            break;
        case Quality::High:
            builder.setNumTaps(16);
            break;
        case Quality::Best:
            builder.setNumTaps(32);
            break;
    }

    // Set the cutoff frequency so that we do not get aliasing when down-sampling.
    if (inputRate > outputRate) {
        builder.setNormalizedCutoff(kDefaultNormalizedCutoff);
    }
    return builder.build();
}

MultiChannelResampler *MultiChannelResampler::Builder::build() {
    if (getNumTaps() == 2) {
        // Note that this does not do low pass filteringh.
        return new LinearResampler(*this);
    }
    IntegerRatio ratio(getInputRate(), getOutputRate());
    ratio.reduce();
    bool usePolyphase = (getNumTaps() * ratio.getDenominator()) <= kMaxCoefficients;
    if (usePolyphase) {
        if (getChannelCount() == 1) {
            return new PolyphaseResamplerMono(*this);
        } else if (getChannelCount() == 2) {
            return new PolyphaseResamplerStereo(*this);
        } else {
            return new PolyphaseResampler(*this);
        }
    } else {
        // Use less optimized resampler that uses a float phaseIncrement.
        // TODO mono resampler
        if (getChannelCount() == 2) {
            return new SincResamplerStereo(*this);
        } else {
            return new SincResampler(*this);
        }
    }
}

void MultiChannelResampler::writeFrame(const float *frame) {
    // Move cursor before write so that cursor points to last written frame in read.
    if (--mCursor < 0) {
        mCursor = getNumTaps() - 1;
    }
    float *dest = &mX[mCursor * getChannelCount()];
    int offset = getNumTaps() * getChannelCount();
    for (int channel = 0; channel < getChannelCount(); channel++) {
        // Write twice so we avoid having to wrap when reading.
        dest[channel] = dest[channel + offset] = frame[channel];
    }
}

float MultiChannelResampler::sinc(float radians) {
    if (abs(radians) < 1.0e-9) return 1.0f;   // avoid divide by zero
    return sinf(radians) / radians;   // Sinc function
}

// Generate coefficients in the order they will be used by readFrame().
// This is more complicated but readFrame() is called repeatedly and should be optimized.
void MultiChannelResampler::generateCoefficients(int32_t inputRate,
                                              int32_t outputRate,
                                              int32_t numRows,
                                              double phaseIncrement,
                                              float normalizedCutoff) {
    mCoefficients.resize(getNumTaps() * numRows);
    int coefficientIndex = 0;
    double phase = 0.0; // ranges from 0.0 to 1.0, fraction between samples
    // Stretch the sinc function for low pass filtering.
    const float cutoffScaler = normalizedCutoff *
            ((outputRate < inputRate)
             ? ((float)outputRate / inputRate)
             : ((float)inputRate / outputRate));
    const int numTapsHalf = getNumTaps() / 2; // numTaps must be even.
    const float numTapsHalfInverse = 1.0f / numTapsHalf;
    for (int i = 0; i < numRows; i++) {
        float tapPhase = phase - numTapsHalf;
        float gain = 0.0; // sum of raw coefficients
        int gainCursor = coefficientIndex;
        for (int tap = 0; tap < getNumTaps(); tap++) {
            float radians = tapPhase * M_PI;

#if MCR_USE_KAISER
            float window = mKaiserWindow(tapPhase * numTapsHalfInverse);
#else
            float window = mCoshWindow(tapPhase * numTapsHalfInverse);
#endif
            float coefficient = sinc(radians * cutoffScaler) * window;
            mCoefficients.at(coefficientIndex++) = coefficient;
            gain += coefficient;
            tapPhase += 1.0;
        }
        phase += phaseIncrement;
        while (phase >= 1.0) {
            phase -= 1.0;
        }

        // Correct for gain variations.
        float gainCorrection = 1.0 / gain; // normalize the gain
        for (int tap = 0; tap < getNumTaps(); tap++) {
            mCoefficients.at(gainCursor + tap) *= gainCorrection;
        }
    }
}
