/*
 * filter_lightshow.cpp -- animate color to the audio
 * Copyright (C) 2015-2020 Meltytech, LLC
 *
 * This library 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.
 *
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "common.h"
#include <framework/mlt.h>
#include <math.h>   // sin()
#include <stdlib.h> // calloc(), free()
#include <QImage>
#include <QPainter>

// Private Constants
static const double PI = 3.14159265358979323846;

namespace {

// Private Types
typedef struct
{
    mlt_filter fft;
    char *mag_prop_name;
    int rel_pos;
    int preprocess_warned;
} private_data;

} // namespace

static int filter_get_audio(mlt_frame frame,
                            void **buffer,
                            mlt_audio_format *format,
                            int *frequency,
                            int *channels,
                            int *samples)
{
    mlt_filter filter = (mlt_filter) mlt_frame_pop_audio(frame);
    mlt_properties filter_properties = MLT_FILTER_PROPERTIES(filter);
    private_data *pdata = (private_data *) filter->child;

    // Create the FFT filter the first time.
    if (!pdata->fft) {
        mlt_profile profile = mlt_service_profile(MLT_FILTER_SERVICE(filter));
        pdata->fft = mlt_factory_filter(profile, "fft", NULL);
        mlt_properties_set_int(MLT_FILTER_PROPERTIES(pdata->fft),
                               "window_size",
                               mlt_properties_get_int(filter_properties, "window_size"));
        if (!pdata->fft) {
            mlt_log_warning(MLT_FILTER_SERVICE(filter), "Unable to create FFT.\n");
            return 1;
        }
    }

    mlt_properties fft_properties = MLT_FILTER_PROPERTIES(pdata->fft);
    double low_freq = mlt_properties_get_int(filter_properties, "frequency_low");
    double hi_freq = mlt_properties_get_int(filter_properties, "frequency_high");
    double threshold = mlt_properties_get_int(filter_properties, "threshold");
    double osc = mlt_properties_get_int(filter_properties, "osc");
    float peak = 0;

    // The service must stay locked while using the private data
    mlt_service_lock(MLT_FILTER_SERVICE(filter));

    // Perform FFT processing on the frame
    mlt_filter_process(pdata->fft, frame);
    mlt_frame_get_audio(frame, buffer, format, frequency, channels, samples);

    float *bins = (float *) mlt_properties_get_data(fft_properties, "bins", NULL);
    double window_level = mlt_properties_get_double(fft_properties, "window_level");

    if (bins && window_level == 1.0) {
        // Find the peak FFT magnitude in the configured range of frequencies
        int bin_count = mlt_properties_get_int(fft_properties, "bin_count");
        double bin_width = mlt_properties_get_double(fft_properties, "bin_width");
        int bin = 0;
        for (bin = 0; bin < bin_count; bin++) {
            double F = bin_width * (double) bin;
            if (F >= low_freq && F <= hi_freq) {
                if (bins[bin] > peak) {
                    peak = bins[bin];
                }
            }
        }
    }

    mlt_service_unlock(MLT_FILTER_SERVICE(filter));

    // Scale the magnitude to dB and apply oscillation
    double dB = peak > 0.0 ? 20 * log10(peak) : -1000.0;
    double mag = 0.0;
    if (dB >= threshold) {
        // Scale to range 0.0-1.0
        mag = 1 - (dB / threshold);
        if (osc != 0) {
            // Apply the oscillation
            double fps = mlt_profile_fps(mlt_service_profile(MLT_FILTER_SERVICE(filter)));
            double t = pdata->rel_pos / fps;
            mag = mag * sin(2 * PI * osc * t);
        }
        pdata->rel_pos++;
    } else {
        pdata->rel_pos = 1;
        mag = 0;
    }

    // Save the magnitude as a property on the frame to be used in get_image()
    mlt_properties_set_double(MLT_FRAME_PROPERTIES(frame), pdata->mag_prop_name, mag);

    return 0;
}

static void setup_pen(QPainter &p, QRect &rect, mlt_properties filter_properties)
{
    QVector<QColor> colors;
    bool color_found = true;

    // Find user specified colors for the gradient
    while (color_found) {
        QString prop_name = QString("color.") + QString::number(colors.size() + 1);
        if (mlt_properties_exists(filter_properties, prop_name.toUtf8().constData())) {
            mlt_color mcolor = mlt_properties_get_color(filter_properties,
                                                        prop_name.toUtf8().constData());
            colors.append(QColor(mcolor.r, mcolor.g, mcolor.b, mcolor.a));
        } else {
            color_found = false;
        }
    }

    if (!colors.size()) {
        // No color specified. Just use white.
        p.setBrush(Qt::white);
    } else if (colors.size() == 1) {
        // Only use one color
        p.setBrush(colors[0]);
    } else {
        // Use Gradient
        qreal sx = 1.0;
        qreal sy = 1.0;
        qreal dx = rect.x();
        qreal dy = rect.y();
        qreal radius = rect.width() / 2;

        if (rect.width() > rect.height()) {
            radius = rect.height() / 2;
            sx = (qreal) rect.width() / (qreal) rect.height();
        } else if (rect.height() > rect.width()) {
            radius = rect.width() / 2;
            sy = (qreal) rect.height() / (qreal) rect.width();
        }

        QPointF center(radius, radius);
        QRadialGradient gradient(center, radius);

        qreal step = 1.0 / (colors.size() - 1);
        for (int i = 0; i < colors.size(); i++) {
            gradient.setColorAt((qreal) i * step, colors[i]);
        }

        QBrush brush(gradient);
        QTransform transform(sx, 0.0, 0.0, 0.0, sy, 0.0, dx, dy, 1.0);
        brush.setTransform(transform);
        p.setBrush(brush);
    }
    p.setPen(QColor(0, 0, 0, 0)); // Clear pen
}

static void draw_light(mlt_properties filter_properties, QImage *qimg, mlt_rect *rect, double mag)
{
    QPainter p(qimg);
    QRect r(rect->x, rect->y, rect->w, rect->h);
    p.setRenderHint(QPainter::Antialiasing);
    // Output transparency = input transparency
    p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
    setup_pen(p, r, filter_properties);
    p.setOpacity(mag);
    p.drawRect(r);
    p.end();
}

/** Get the image.
*/
static int filter_get_image(mlt_frame frame,
                            uint8_t **image,
                            mlt_image_format *format,
                            int *width,
                            int *height,
                            int writable)
{
    int error = 0;
    mlt_filter filter = (mlt_filter) mlt_frame_pop_service(frame);

    private_data *pdata = (private_data *) filter->child;
    mlt_properties filter_properties = MLT_FILTER_PROPERTIES(filter);
    mlt_properties frame_properties = MLT_FRAME_PROPERTIES(frame);

    if (mlt_properties_exists(frame_properties, pdata->mag_prop_name)) {
        double mag = mlt_properties_get_double(frame_properties, pdata->mag_prop_name);
        mlt_position position = mlt_filter_get_position(filter, frame);
        mlt_position length = mlt_filter_get_length2(filter, frame);
        mlt_rect rect = mlt_properties_anim_get_rect(filter_properties, "rect", position, length);

        // Get the current image
        *format = mlt_image_rgba;
        error = mlt_frame_get_image(frame, image, format, width, height, 1);

        if (strchr(mlt_properties_get(filter_properties, "rect"), '%')) {
            rect.x *= *width;
            rect.w *= *width;
            rect.y *= *height;
            rect.h *= *height;
        } else {
            mlt_profile profile = mlt_service_profile(MLT_FILTER_SERVICE(filter));
            double scale = mlt_profile_scale_width(profile, *width);
            rect.x *= scale;
            rect.w *= scale;
            scale = mlt_profile_scale_height(profile, *height);
            rect.y *= scale;
            rect.h *= scale;
        }

        // Draw the light
        if (!error) {
            QImage qimg(*width, *height, QImage::Format_ARGB32);
            convert_mlt_to_qimage_rgba(*image, &qimg, *width, *height);
            draw_light(filter_properties, &qimg, &rect, mag);
            convert_qimage_to_mlt_rgba(&qimg, *image, *width, *height);
        }
    } else {
        if (pdata->preprocess_warned++ == 2) {
            // This filter depends on the consumer processing the audio before
            // the video.
            mlt_log_warning(MLT_FILTER_SERVICE(filter), "Audio not preprocessed.\n");
        }
        mlt_frame_get_image(frame, image, format, width, height, writable);
    }

    return error;
}

/** Filter processing.
*/
static mlt_frame filter_process(mlt_filter filter, mlt_frame frame)
{
    if (mlt_frame_is_test_card(frame)) {
        // The producer does not generate video. This filter will create an
        // image on the producer's behalf.
        mlt_properties frame_properties = MLT_FRAME_PROPERTIES(frame);
        mlt_profile profile = mlt_service_profile(MLT_FILTER_SERVICE(filter));
        mlt_properties_set_int(frame_properties, "progressive", 1);
        mlt_properties_set_double(frame_properties, "aspect_ratio", mlt_profile_sar(profile));
        mlt_properties_set_int(frame_properties, "meta.media.width", profile->width);
        mlt_properties_set_int(frame_properties, "meta.media.height", profile->height);
        // Tell the framework that there really is an image.
        mlt_properties_set_int(frame_properties, "test_image", 0);
        // Push a callback to create the image.
        mlt_frame_push_get_image(frame, create_image);
    }

    mlt_frame_push_audio(frame, filter);
    mlt_frame_push_audio(frame, (void *) filter_get_audio);
    mlt_frame_push_service(frame, filter);
    mlt_frame_push_get_image(frame, filter_get_image);
    return frame;
}

static void filter_close(mlt_filter filter)
{
    private_data *pdata = (private_data *) filter->child;

    if (pdata) {
        mlt_filter_close(pdata->fft);
        free(pdata->mag_prop_name);
        free(pdata);
    }
    filter->child = NULL;
    filter->close = NULL;
    filter->parent.close = NULL;
    mlt_service_close(&filter->parent);
}

/** Constructor for the filter.
*/

extern "C" {

mlt_filter filter_lightshow_init(mlt_profile profile,
                                 mlt_service_type type,
                                 const char *id,
                                 char *arg)
{
    mlt_filter filter = mlt_filter_new();
    private_data *pdata = (private_data *) calloc(1, sizeof(private_data));

    if (filter && pdata && createQApplicationIfNeeded(MLT_FILTER_SERVICE(filter))) {
        mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
        mlt_properties_set_int(properties, "_filter_private", 1);
        mlt_properties_set_int(properties, "frequency_low", 20);
        mlt_properties_set_int(properties, "frequency_high", 20000);
        mlt_properties_set_double(properties, "threshold", -30.0);
        mlt_properties_set_double(properties, "osc", 5.0);
        mlt_properties_set(properties, "color.1", "0xffffffff");
        mlt_properties_set(properties, "rect", "0% 0% 100% 100%");
        mlt_properties_set_int(properties, "window_size", 2048);

        // Create a unique ID for storing data on the frame
        pdata->mag_prop_name = (char *) calloc(1, 20);
        snprintf(pdata->mag_prop_name, 20, "fft_mag.%p", filter);
        pdata->mag_prop_name[20 - 1] = '\0';

        pdata->fft = 0;

        filter->close = filter_close;
        filter->process = filter_process;
        filter->child = pdata;
    } else {
        mlt_log_error(MLT_FILTER_SERVICE(filter), "Filter lightshow failed\n");

        if (filter) {
            mlt_filter_close(filter);
        }

        if (pdata) {
            free(pdata);
        }

        filter = NULL;
    }
    return filter;
}
}
