/*
 * This file is part of mpv.
 *
 * mpv is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * mpv 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <assert.h>

#include <rubberband/rubberband-c.h>

#include "config.h"

#include "audio/aframe.h"
#include "audio/format.h"
#include "common/common.h"
#include "filters/f_autoconvert.h"
#include "filters/filter_internal.h"
#include "filters/user_filters.h"
#include "options/m_option.h"

// command line options
struct f_opts {
    int transients, detector, phase, window,
        smoothing, formant, pitch, channels, engine;
    double scale;
};

struct priv {
    struct f_opts *opts;

    struct mp_pin *in_pin;
    struct mp_aframe *cur_format;
    struct mp_aframe_pool *out_pool;
    bool sent_final;
    RubberBandState rubber;
    double speed;
    double pitch;
    struct mp_aframe *pending;
    // Estimate how much librubberband has buffered internally.
    // I could not find a way to do this with the librubberband API.
    double rubber_delay;
};

static void update_speed(struct priv *p, double new_speed)
{
    p->speed = new_speed;
    if (p->rubber)
        rubberband_set_time_ratio(p->rubber, 1.0 / p->speed);
}

static bool update_pitch(struct priv *p, double new_pitch)
{
    if (new_pitch < 0.01 || new_pitch > 100.0)
        return false;

    p->pitch = new_pitch;
    if (p->rubber)
        rubberband_set_pitch_scale(p->rubber, p->pitch);
    return true;
}

static bool init_rubberband(struct mp_filter *f)
{
    struct priv *p = f->priv;

    mp_assert(!p->rubber);
    mp_assert(p->pending);

    int opts = p->opts->transients | p->opts->detector | p->opts->phase |
               p->opts->window | p->opts->smoothing | p->opts->formant |
               p->opts->pitch | p->opts->channels |
#if HAVE_RUBBERBAND_3
               p->opts->engine |
#endif
               RubberBandOptionProcessRealTime;

    int rate = mp_aframe_get_rate(p->pending);
    int channels = mp_aframe_get_channels(p->pending);
    if (mp_aframe_get_format(p->pending) != AF_FORMAT_FLOATP)
        return false;

    p->rubber = rubberband_new(rate, channels, opts, 1.0, 1.0);
    if (!p->rubber) {
        MP_FATAL(f, "librubberband initialization failed.\n");
        return false;
    }

    mp_aframe_config_copy(p->cur_format, p->pending);

    update_speed(p, p->speed);
    update_pitch(p, p->pitch);

    return true;
}

static void af_rubberband_process(struct mp_filter *f)
{
    struct priv *p = f->priv;

    if (!mp_pin_in_needs_data(f->ppins[1]))
        return;

    while (!p->rubber || !p->pending || rubberband_available(p->rubber) <= 0) {
        const float *dummy[MP_NUM_CHANNELS] = {0};
        const float **in_data = dummy;
        size_t in_samples = 0;

        bool eof = false;
        if (!p->pending || !mp_aframe_get_size(p->pending)) {
            struct mp_frame frame = mp_pin_out_read(p->in_pin);
            if (frame.type == MP_FRAME_AUDIO) {
                TA_FREEP(&p->pending);
                p->pending = frame.data;
            } else if (frame.type == MP_FRAME_EOF) {
                eof = true;
            } else if (frame.type) {
                MP_ERR(f, "unexpected frame type\n");
                goto error;
            } else {
                return; // no new data yet
            }
        }
        mp_assert(p->pending || eof);

        if (!p->rubber) {
            if (!p->pending) {
                mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
                return;
            }
            if (!init_rubberband(f))
                goto error;
        }

        bool format_change =
            p->pending && !mp_aframe_config_equals(p->pending, p->cur_format);

        if (p->pending && !format_change) {
            size_t needs = rubberband_get_samples_required(p->rubber);
            uint8_t **planes = mp_aframe_get_data_ro(p->pending);
            int num_planes = mp_aframe_get_planes(p->pending);
            for (int n = 0; n < num_planes; n++)
                in_data[n] = (void *)planes[n];
            in_samples = MPMIN(mp_aframe_get_size(p->pending), needs);
        }

        bool final = format_change || eof;
        if (!p->sent_final)
            rubberband_process(p->rubber, in_data, in_samples, final);
        p->sent_final |= final;

        p->rubber_delay += in_samples;

        if (p->pending && !format_change)
            mp_aframe_skip_samples(p->pending, in_samples);

        if (rubberband_available(p->rubber) > 0) {
            if (eof)
                mp_pin_out_repeat_eof(p->in_pin); // drain more next time
        } else {
            if (eof) {
                mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
                rubberband_reset(p->rubber);
                p->rubber_delay = 0;
                TA_FREEP(&p->pending);
                p->sent_final = false;
                return;
            } else if (format_change) {
                // go on with proper reinit on the next iteration
                rubberband_delete(p->rubber);
                p->sent_final = false;
                p->rubber = NULL;
            }
        }
    }

    mp_assert(p->pending);

    int out_samples = rubberband_available(p->rubber);
    if (out_samples > 0) {
        struct mp_aframe *out = mp_aframe_new_ref(p->cur_format);
        if (mp_aframe_pool_allocate(p->out_pool, out, out_samples) < 0) {
            talloc_free(out);
            goto error;
        }

        mp_aframe_copy_attributes(out, p->pending);

        float *out_data[MP_NUM_CHANNELS] = {0};
        uint8_t **planes = mp_aframe_get_data_rw(out);
        mp_assert(planes);
        int num_planes = mp_aframe_get_planes(out);
        for (int n = 0; n < num_planes; n++)
            out_data[n] = (void *)planes[n];

        out_samples = rubberband_retrieve(p->rubber, out_data, out_samples);

        if (!out_samples) {
            mp_filter_internal_mark_progress(f); // unexpected, just try again
            talloc_free(out);
            return;
        }

        mp_aframe_set_size(out, out_samples);

        p->rubber_delay -= out_samples * p->speed;

        double pts = mp_aframe_get_pts(p->pending);
        if (pts != MP_NOPTS_VALUE) {
            // Note: rubberband_get_latency() does not do what you'd expect.
            double delay = p->rubber_delay / mp_aframe_get_effective_rate(out);
            mp_aframe_set_pts(out, pts - delay);
        }

        mp_aframe_mul_speed(out, p->speed);

        mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out));
    }

    return;
error:
    mp_filter_internal_mark_failed(f);
}

static bool af_rubberband_command(struct mp_filter *f, struct mp_filter_command *cmd)
{
    struct priv *p = f->priv;

    switch (cmd->type) {
    case MP_FILTER_COMMAND_TEXT: {
        char *endptr = NULL;
        double pitch = p->pitch;
        if (!strcmp(cmd->cmd, "set-pitch")) {
            pitch = strtod(cmd->arg, &endptr);
            if (*endptr)
                return false;
            return update_pitch(p, pitch);
        } else if (!strcmp(cmd->cmd, "multiply-pitch")) {
            double mult = strtod(cmd->arg, &endptr);
            if (*endptr || mult <= 0)
                return false;
            pitch *= mult;
            return update_pitch(p, pitch);
        }
        return false;
    }
    case MP_FILTER_COMMAND_SET_SPEED:
        update_speed(p, cmd->speed);
        return true;
    }

    return false;
}

static void af_rubberband_reset(struct mp_filter *f)
{
    struct priv *p = f->priv;

    if (p->rubber)
        rubberband_reset(p->rubber);
    p->rubber_delay = 0;
    p->sent_final = false;
    TA_FREEP(&p->pending);
}

static void af_rubberband_destroy(struct mp_filter *f)
{
    struct priv *p = f->priv;

    if (p->rubber)
        rubberband_delete(p->rubber);
    talloc_free(p->pending);
}

static const struct mp_filter_info af_rubberband_filter = {
    .name = "rubberband",
    .priv_size = sizeof(struct priv),
    .process = af_rubberband_process,
    .command = af_rubberband_command,
    .reset = af_rubberband_reset,
    .destroy = af_rubberband_destroy,
};

static struct mp_filter *af_rubberband_create(struct mp_filter *parent,
                                              void *options)
{
    struct mp_filter *f = mp_filter_create(parent, &af_rubberband_filter);
    if (!f) {
        talloc_free(options);
        return NULL;
    }

    mp_filter_add_pin(f, MP_PIN_IN, "in");
    mp_filter_add_pin(f, MP_PIN_OUT, "out");

    struct priv *p = f->priv;
    p->opts = talloc_steal(p, options);
    p->speed = 1.0;
    p->pitch = p->opts->scale;
    p->cur_format = talloc_steal(p, mp_aframe_create());
    p->out_pool = mp_aframe_pool_create(p);

    struct mp_autoconvert *conv = mp_autoconvert_create(f);
    if (!conv)
        abort();

    mp_autoconvert_add_afmt(conv, AF_FORMAT_FLOATP);

    mp_pin_connect(conv->f->pins[0], f->ppins[0]);
    p->in_pin = conv->f->pins[1];

    return f;
}

#define OPT_BASE_STRUCT struct f_opts

const struct mp_user_filter_entry af_rubberband = {
    .desc = {
        .description = "Pitch conversion with librubberband",
        .name = "rubberband",
        .priv_size = sizeof(OPT_BASE_STRUCT),
        .priv_defaults = &(const OPT_BASE_STRUCT) {
            .scale = 1.0,
            .pitch = RubberBandOptionPitchHighConsistency,
            .transients = RubberBandOptionTransientsMixed,
            .formant = RubberBandOptionFormantPreserved,
            .channels = RubberBandOptionChannelsTogether,
#if HAVE_RUBBERBAND_3
            .engine = RubberBandOptionEngineFiner,
#endif
        },
        .options = (const struct m_option[]) {
            {"transients", OPT_CHOICE(transients,
                {"crisp", RubberBandOptionTransientsCrisp},
                {"mixed", RubberBandOptionTransientsMixed},
                {"smooth", RubberBandOptionTransientsSmooth})},
            {"detector", OPT_CHOICE(detector,
                {"compound", RubberBandOptionDetectorCompound},
                {"percussive", RubberBandOptionDetectorPercussive},
                {"soft", RubberBandOptionDetectorSoft})},
            {"phase", OPT_CHOICE(phase,
                {"laminar", RubberBandOptionPhaseLaminar},
                {"independent", RubberBandOptionPhaseIndependent})},
            {"window", OPT_CHOICE(window,
                {"standard", RubberBandOptionWindowStandard},
                {"short", RubberBandOptionWindowShort},
                {"long", RubberBandOptionWindowLong})},
            {"smoothing", OPT_CHOICE(smoothing,
                {"off", RubberBandOptionSmoothingOff},
                {"on", RubberBandOptionSmoothingOn})},
            {"formant", OPT_CHOICE(formant,
                {"shifted", RubberBandOptionFormantShifted},
                {"preserved", RubberBandOptionFormantPreserved})},
            {"pitch", OPT_CHOICE(pitch,
                {"quality", RubberBandOptionPitchHighQuality},
                {"speed", RubberBandOptionPitchHighSpeed},
                {"consistency", RubberBandOptionPitchHighConsistency})},
            {"channels", OPT_CHOICE(channels,
                {"apart", RubberBandOptionChannelsApart},
                {"together", RubberBandOptionChannelsTogether})},
#if HAVE_RUBBERBAND_3
            {"engine", OPT_CHOICE(engine,
                {"finer", RubberBandOptionEngineFiner},
                {"faster", RubberBandOptionEngineFaster})},
#endif
            {"pitch-scale", OPT_DOUBLE(scale), M_RANGE(0.01, 100)},
            {0}
        },
    },
    .create = af_rubberband_create,
};
