/*
 * Carla Native Plugins
 * Copyright (C) 2012-2019 Filipe Coelho <falktx@falktx.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * For a full copy of the GNU General Public License see the doc/GPL.txt file.
 */

#include "CarlaNative.h"
#include "CarlaMIDI.h"

#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

#define MAX_CHANNELS 2

// -----------------------------------------------------------------------

typedef struct {
    float a0, b1, z1;
} Filter;

static inline
void set_filter_sample_rate(Filter* const filter, const float sampleRate)
{
    static const float M_PIf = (float)M_PI;
    const float frequency = 30.0f / sampleRate;

    filter->b1 = expf(-2.0f * M_PIf * frequency);
    filter->a0 = 1.0f - filter->b1;
    filter->z1 = 0.0f;
}

// -----------------------------------------------------------------------

typedef enum {
    // all versions
    PARAM_GAIN = 0,
    PARAM_COUNT_MONO,

    // stereo version
    PARAM_APPLY_LEFT = PARAM_COUNT_MONO,
    PARAM_APPLY_RIGHT,
    PARAM_COUNT_STEREO
} AudioGainParams;

typedef struct {
    Filter lowpass[MAX_CHANNELS];
    float gain;
    bool isMono;
    bool applyLeft;
    bool applyRight;
} AudioGainHandle;

// -----------------------------------------------------------------------

static NativePluginHandle audiogain_instantiate(const NativeHostDescriptor* host, const bool isMono)
{
    AudioGainHandle* const handle = (AudioGainHandle*)malloc(sizeof(AudioGainHandle));

    if (handle == NULL)
        return NULL;

    handle->gain = 1.0f;
    handle->isMono = isMono;
    handle->applyLeft = true;
    handle->applyRight = true;

    const float sampleRate = (float)host->get_sample_rate(host->handle);

    for (unsigned i = 0; i < MAX_CHANNELS; ++i)
        set_filter_sample_rate(&handle->lowpass[i], sampleRate);

    return handle;
}

static NativePluginHandle audiogain_instantiate_mono(const NativeHostDescriptor* host)
{
    return audiogain_instantiate(host, true);
}

static NativePluginHandle audiogain_instantiate_stereo(const NativeHostDescriptor* host)
{
    return audiogain_instantiate(host, false);
}

#define handlePtr ((AudioGainHandle*)handle)

static void audiogain_cleanup(NativePluginHandle handle)
{
    free(handlePtr);
}

static uint32_t audiogain_get_parameter_count(NativePluginHandle handle)
{
    return handlePtr->isMono ? PARAM_COUNT_MONO : PARAM_COUNT_STEREO;
}

static const NativeParameter* audiogain_get_parameter_info(NativePluginHandle handle, uint32_t index)
{
    if (index > (handlePtr->isMono ? PARAM_COUNT_MONO : PARAM_COUNT_STEREO))
        return NULL;

    static NativeParameter param;

    param.hints = NATIVE_PARAMETER_IS_ENABLED|NATIVE_PARAMETER_IS_AUTOMATABLE;
    param.unit  = NULL;
    param.scalePointCount = 0;
    param.scalePoints     = NULL;

    switch (index)
    {
    case PARAM_GAIN:
        param.name = "Gain";
        param.ranges.def = 1.0f;
        param.ranges.min = 0.0f;
        param.ranges.max = 4.0f;
        param.ranges.step = PARAMETER_RANGES_DEFAULT_STEP;
        param.ranges.stepSmall = PARAMETER_RANGES_DEFAULT_STEP_SMALL;
        param.ranges.stepLarge = PARAMETER_RANGES_DEFAULT_STEP_LARGE;
        break;
    case PARAM_APPLY_LEFT:
        param.name   = "Apply Left";
        param.hints |= NATIVE_PARAMETER_IS_BOOLEAN;
        param.ranges.def = 1.0f;
        param.ranges.min = 0.0f;
        param.ranges.max = 1.0f;
        param.ranges.step = 1.0f;
        param.ranges.stepSmall = 1.0f;
        param.ranges.stepLarge = 1.0f;
        break;
    case PARAM_APPLY_RIGHT:
        param.name   = "Apply Right";
        param.hints |= NATIVE_PARAMETER_IS_BOOLEAN;
        param.ranges.def = 1.0f;
        param.ranges.min = 0.0f;
        param.ranges.max = 1.0f;
        param.ranges.step = 1.0f;
        param.ranges.stepSmall = 1.0f;
        param.ranges.stepLarge = 1.0f;
        break;
    }

    return &param;
}

static float audiogain_get_parameter_value(NativePluginHandle handle, uint32_t index)
{
    switch (index)
    {
    case PARAM_GAIN:
        return handlePtr->gain;
    case PARAM_APPLY_LEFT:
        return handlePtr->applyLeft ? 1.0f : 0.0f;
    case PARAM_APPLY_RIGHT:
        return handlePtr->applyRight ? 1.0f : 0.0f;
    default:
        return 0.0f;
    }
}

static void audiogain_set_parameter_value(NativePluginHandle handle, uint32_t index, float value)
{
    switch (index)
    {
    case PARAM_GAIN:
        handlePtr->gain = value;
        break;
    case PARAM_APPLY_LEFT:
        handlePtr->applyLeft = (value >= 0.5f);
        break;
    case PARAM_APPLY_RIGHT:
        handlePtr->applyRight = (value >= 0.5f);
        break;
    }
}

static inline
void handle_audio_buffers(const float* inBuffer, float* outBuffer, Filter* const filter, const float gain, const uint32_t frames)
{
    const float a0 = filter->a0;
    const float b1 = filter->b1;
    float z1 = filter->z1;

    for (uint32_t i=0; i < frames; ++i) {
        z1 = gain * a0 + z1 * b1;
        *outBuffer++ = *inBuffer++ * z1;
    }

    filter->z1 = z1;
}

// FIXME for v3.0, use const for the input buffer
static void audiogain_process(NativePluginHandle handle,
                             float** inBuffer, float** outBuffer, uint32_t frames,
                             const NativeMidiEvent* midiEvents, uint32_t midiEventCount)
{
    const float gain      = handlePtr->gain;
    const bool applyLeft  = handlePtr->applyLeft;
    const bool applyRight = handlePtr->applyRight;
    const bool isMono     = handlePtr->isMono;

    handle_audio_buffers(inBuffer[0], outBuffer[0], &handlePtr->lowpass[0], (isMono || applyLeft) ? gain : 1.0f, frames);

    if (! isMono)
        handle_audio_buffers(inBuffer[1], outBuffer[1], &handlePtr->lowpass[1], applyRight ? gain : 1.0f, frames);

    return;

    // unused
    (void)midiEvents;
    (void)midiEventCount;
}

static intptr_t audiogain_dispatcher(NativePluginHandle handle, NativePluginDispatcherOpcode opcode, int32_t index, intptr_t value, void* ptr, float opt)
{
    switch (opcode)
    {
    case NATIVE_PLUGIN_OPCODE_SAMPLE_RATE_CHANGED:
        for (unsigned i = 0; i < MAX_CHANNELS; ++i)
            set_filter_sample_rate(&handlePtr->lowpass[i], opt);
        break;
    default:
        break;
    }

    return 0;

    // unused
    (void)index;
    (void)value;
    (void)ptr;
}

// -----------------------------------------------------------------------

static const NativePluginDescriptor audiogainMonoDesc = {
    .category  = NATIVE_PLUGIN_CATEGORY_UTILITY,
    .hints     = NATIVE_PLUGIN_IS_RTSAFE,
    .supports  = NATIVE_PLUGIN_SUPPORTS_NOTHING,
    .audioIns  = 1,
    .audioOuts = 1,
    .midiIns   = 0,
    .midiOuts  = 0,
    .paramIns  = PARAM_COUNT_MONO,
    .paramOuts = 0,
    .name      = "Audio Gain (Mono)",
    .label     = "audiogain",
    .maker     = "falkTX",
    .copyright = "GNU GPL v2+",

    .instantiate = audiogain_instantiate_mono,
    .cleanup     = audiogain_cleanup,

    .get_parameter_count = audiogain_get_parameter_count,
    .get_parameter_info  = audiogain_get_parameter_info,
    .get_parameter_value = audiogain_get_parameter_value,

    .get_midi_program_count = NULL,
    .get_midi_program_info  = NULL,

    .set_parameter_value = audiogain_set_parameter_value,
    .set_midi_program    = NULL,
    .set_custom_data     = NULL,

    .ui_show = NULL,
    .ui_idle = NULL,

    .ui_set_parameter_value = NULL,
    .ui_set_midi_program    = NULL,
    .ui_set_custom_data     = NULL,

    .activate   = NULL,
    .deactivate = NULL,
    .process    = audiogain_process,

    .get_state = NULL,
    .set_state = NULL,

    .dispatcher = audiogain_dispatcher,

    .render_inline_display = NULL
};

static const NativePluginDescriptor audiogainStereoDesc = {
    .category  = NATIVE_PLUGIN_CATEGORY_UTILITY,
    .hints     = NATIVE_PLUGIN_IS_RTSAFE,
    .supports  = NATIVE_PLUGIN_SUPPORTS_NOTHING,
    .audioIns  = 2,
    .audioOuts = 2,
    .cvIns     = 0,
    .cvOuts    = 0,
    .midiIns   = 0,
    .midiOuts  = 0,
    .paramIns  = PARAM_COUNT_STEREO,
    .paramOuts = 0,
    .name      = "Audio Gain (Stereo)",
    .label     = "audiogain_s",
    .maker     = "falkTX",
    .copyright = "GNU GPL v2+",

    .instantiate = audiogain_instantiate_stereo,
    .cleanup     = audiogain_cleanup,

    .get_parameter_count = audiogain_get_parameter_count,
    .get_parameter_info  = audiogain_get_parameter_info,
    .get_parameter_value = audiogain_get_parameter_value,

    .get_midi_program_count = NULL,
    .get_midi_program_info  = NULL,

    .set_parameter_value = audiogain_set_parameter_value,
    .set_midi_program    = NULL,
    .set_custom_data     = NULL,

    .ui_show = NULL,
    .ui_idle = NULL,

    .ui_set_parameter_value = NULL,
    .ui_set_midi_program    = NULL,
    .ui_set_custom_data     = NULL,

    .activate   = NULL,
    .deactivate = NULL,
    .process    = audiogain_process,

    .get_state = NULL,
    .set_state = NULL,

    .dispatcher = audiogain_dispatcher
};

// -----------------------------------------------------------------------

void carla_register_native_plugin_audiogain(void);

void carla_register_native_plugin_audiogain(void)
{
    carla_register_native_plugin(&audiogainMonoDesc);
    carla_register_native_plugin(&audiogainStereoDesc);
}

// -----------------------------------------------------------------------
