/*
 * This file is part of libplacebo.
 *
 * libplacebo 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.
 *
 * libplacebo 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 libplacebo. If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Some of the filter code originally derives (via mpv) from Glumpy:
 * # Copyright (c) 2009-2016 Nicolas P. Rougier. All rights reserved.
 * # Distributed under the (new) BSD License.
 * (https://github.com/glumpy/glumpy/blob/master/glumpy/library/build-spatial-filters.py)
 *
 * The math underlying each filter function was written from scratch, with
 * some algorithms coming from a number of different sources, including:
 * - https://en.wikipedia.org/wiki/Window_function
 * - https://en.wikipedia.org/wiki/Jinc
 * - http://vector-agg.cvs.sourceforge.net/viewvc/vector-agg/agg-2.5/include/agg_image_filters.h
 * - Vapoursynth plugin fmtconv (WTFPL Licensed), which is based on
 *   dither plugin for avisynth from the same author:
 *   https://github.com/vapoursynth/fmtconv/tree/master/src/fmtc
 * - Paul Heckbert's "zoom"
 * - XBMC: ConvolutionKernels.cpp etc.
 * - https://github.com/AviSynth/jinc-resize (only used to verify the math)
 */

#include <math.h>

#include "common.h"
#include "filters.h"
#include "log.h"

#ifdef PL_HAVE_WIN32
#define j1 _j1
#endif

bool pl_filter_function_eq(const struct pl_filter_function *a,
                           const struct pl_filter_function *b)
{
    return (a ? a->weight : NULL) == (b ? b->weight : NULL);
}

bool pl_filter_config_eq(const struct pl_filter_config *a,
                         const struct pl_filter_config *b)
{
    if (!a || !b)
        return a == b;

    bool eq = pl_filter_function_eq(a->kernel, b->kernel) &&
              pl_filter_function_eq(a->window, b->window) &&
              a->radius   == b->radius &&
              a->clamp    == b->clamp  &&
              a->blur     == b->blur   &&
              a->taper    == b->taper  &&
              a->polar    == b->polar  &&
              a->antiring == b->antiring;

    for (int i = 0; i < PL_FILTER_MAX_PARAMS; i++) {
        if (a->kernel->tunable[i])
            eq &= a->params[i] == b->params[i];
        if (a->window && a->window->tunable[i])
            eq &= a->wparams[i] == b->wparams[i];
    }

    return eq;
}

double pl_filter_sample(const struct pl_filter_config *c, double x)
{
    const float radius = pl_filter_radius_bound(c);

    // All filters are symmetric, and in particular only need to be defined
    // for [0, radius].
    x = fabs(x);

    // Return early for values outside of the kernel radius, since the functions
    // are not necessarily valid outside of this interval. No such check is
    // needed for the window, because it's always stretched to fit.
    if (x > radius)
        return 0.0;

    // Apply the blur and taper coefficients as needed
    double kx = x <= c->taper ? 0.0 : (x - c->taper) / (1.0 - c->taper / radius);
    if (c->blur > 0.0)
        kx /= c->blur;

    pl_assert(!c->kernel->opaque);
    double k = c->kernel->weight(&(const struct pl_filter_ctx) {
        .radius = radius,
        .params = {
            c->kernel->tunable[0] ? c->params[0] : c->kernel->params[0],
            c->kernel->tunable[1] ? c->params[1] : c->kernel->params[1],
        },
    }, kx);

    // Apply the optional windowing function
    if (c->window) {
        pl_assert(!c->window->opaque);
        double wx = x / radius * c->window->radius;
        k *= c->window->weight(&(struct pl_filter_ctx) {
            .radius = c->window->radius,
            .params = {
                c->window->tunable[0] ? c->wparams[0] : c->window->params[0],
                c->window->tunable[1] ? c->wparams[1] : c->window->params[1],
            },
        }, wx);
    }

    return k < 0 ? (1 - c->clamp) * k : k;
}

static void filter_cutoffs(const struct pl_filter_config *c, float cutoff,
                           float *out_radius, float *out_radius_zero)
{
    const float bound = pl_filter_radius_bound(c);
    float prev = 0.0, fprev = pl_filter_sample(c, prev);
    bool found_root = false;

    const float step = 1e-2f;
    for (float x = 0.0; x < bound + step; x += step) {
        float fx = pl_filter_sample(c, x);
        if ((fprev > cutoff && fx <= cutoff) || (fprev < -cutoff && fx >= -cutoff)) {
            // Found zero crossing
            float root = x - fx * (x - prev) / (fx - fprev); // secant method
            root = fminf(root, bound);
            *out_radius = root;
            if (!found_root) // first root
                *out_radius_zero = root;
            found_root = true;
        }
        prev = x;
        fprev = fx;
    }

    if (!found_root)
        *out_radius_zero = *out_radius = bound;
}

// Compute a single row of weights for a given filter in one dimension, indexed
// by the indicated subpixel offset. Writes `f->row_size` values to `out`.
static void compute_row(struct pl_filter_t *f, double offset, float *out)
{
    double wsum = 0.0;
    for (int i = 0; i < f->row_size; i++) {
        // For the example of a filter with row size 4 and offset 0.3, we have:
        //
        // 0    1 *  2    3
        //
        // * indicates the sampled position. What we want to compute is the
        // distance from each index to that sampled position.
        pl_assert(f->row_size % 2 == 0);
        const int base = f->row_size / 2 - 1; // index to the left of the center
        const double center = base + offset; // offset of center relative to idx 0
        double w = pl_filter_sample(&f->params.config, i - center);
        out[i] = w;
        wsum += w;
    }

    // Readjust weights to preserve energy
    pl_assert(wsum > 0);
    for (int i = 0; i < f->row_size; i++)
        out[i] /= wsum;
}

// Needed for backwards compatibility with v1 configuration API
static struct pl_filter_function *dupfilter(void *alloc,
                                            const struct pl_filter_function *f)
{
    return f ? pl_memdup(alloc, (void *)f, sizeof(*f)) : NULL;
}

pl_filter pl_filter_generate(pl_log log, const struct pl_filter_params *params)
{
    pl_assert(params);
    if (params->lut_entries <= 0 || !params->config.kernel) {
        pl_fatal(log, "Invalid params: missing lut_entries or config.kernel");
        return NULL;
    }

    if (params->config.kernel->opaque) {
        pl_err(log, "Trying to use opaque kernel '%s' in non-opaque context!",
               params->config.kernel->name);
        return NULL;
    }

    if (params->config.window && params->config.window->opaque) {
        pl_err(log, "Trying to use opaque window '%s' in non-opaque context!",
               params->config.window->name);
        return NULL;
    }

    struct pl_filter_t *f = pl_zalloc_ptr(NULL, f);
    f->params = *params;
    f->params.config.kernel = dupfilter(f, params->config.kernel);
    f->params.config.window = dupfilter(f, params->config.window);

    // Compute main lobe and total filter size
    filter_cutoffs(&params->config, params->cutoff, &f->radius, &f->radius_zero);
    f->radius_cutoff = f->radius; // backwards compatibility

    float *weights;
    if (params->config.polar) {
        // Compute a 1D array indexed by radius
        weights = pl_alloc(f, params->lut_entries * sizeof(float));
        for (int i = 0; i < params->lut_entries; i++) {
            double x = f->radius * i / (params->lut_entries - 1);
            weights[i] = pl_filter_sample(&params->config, x);
        }
    } else {
        // Pick the most appropriate row size
        f->row_size = ceilf(f->radius) * 2;
        if (params->max_row_size && f->row_size > params->max_row_size) {
            pl_info(log, "Required filter size %d exceeds the maximum allowed "
                    "size of %d. This may result in adverse effects (aliasing, "
                    "or moiré artifacts).", f->row_size, params->max_row_size);
            f->row_size = params->max_row_size;
            f->insufficient = true;
        }
        f->row_stride = PL_ALIGN(f->row_size, params->row_stride_align);

        // Compute a 2D array indexed by the subpixel position
        weights = pl_calloc(f, params->lut_entries * f->row_stride, sizeof(float));
        for (int i = 0; i < params->lut_entries; i++) {
            compute_row(f, i / (double)(params->lut_entries - 1),
                        weights + f->row_stride * i);
        }
    }

    f->weights = weights;
    return f;
}

void pl_filter_free(pl_filter *filter)
{
    pl_free_ptr((void **) filter);
}

// Built-in filter functions

static double box(const struct pl_filter_ctx *f, double x)
{
    return 1.0;
}

const struct pl_filter_function pl_filter_function_box = {
    .weight    = box,
    .name      = "box",
    .radius    = 1.0,
    .resizable = true,
};

static const struct pl_filter_function filter_function_dirichlet = {
    .name      = "dirichlet", // alias
    .weight    = box,
    .radius    = 1.0,
    .resizable = true,
};

static double triangle(const struct pl_filter_ctx *f, double x)
{
    return 1.0 - x / f->radius;
}

const struct pl_filter_function pl_filter_function_triangle = {
    .name      = "triangle",
    .weight    = triangle,
    .radius    = 1.0,
    .resizable = true,
};

static double cosine(const struct pl_filter_ctx *f, double x)
{
    return cos(x);
}

const struct pl_filter_function pl_filter_function_cosine = {
    .name   = "cosine",
    .weight = cosine,
    .radius = M_PI / 2.0,
};

static double hann(const struct pl_filter_ctx *f, double x)
{
    return 0.5 + 0.5 * cos(M_PI * x);
}

const struct pl_filter_function pl_filter_function_hann = {
    .name   = "hann",
    .weight = hann,
    .radius = 1.0,
};

static const struct pl_filter_function filter_function_hanning = {
    .name   = "hanning", // alias
    .weight = hann,
    .radius = 1.0,
};

static double hamming(const struct pl_filter_ctx *f, double x)
{
    return 0.54 + 0.46 * cos(M_PI * x);
}

const struct pl_filter_function pl_filter_function_hamming = {
    .name   = "hamming",
    .weight = hamming,
    .radius = 1.0,
};

static double welch(const struct pl_filter_ctx *f, double x)
{
    return 1.0 - x * x;
}

const struct pl_filter_function pl_filter_function_welch = {
    .name   = "welch",
    .weight = welch,
    .radius = 1.0,
};

static double bessel_i0(double x)
{
    double s = 1.0;
    double y = x * x / 4.0;
    double t = y;
    int i = 2;
    while (t > 1e-12) {
        s += t;
        t *= y / (i * i);
        i += 1;
    }
    return s;
}

static double kaiser(const struct pl_filter_ctx *f, double x)
{
    double alpha = fmax(f->params[0], 0.0);
    double scale = bessel_i0(alpha);
    return bessel_i0(alpha * sqrt(1.0 - x * x)) / scale;
}

const struct pl_filter_function pl_filter_function_kaiser = {
    .name    = "kaiser",
    .weight  = kaiser,
    .radius  = 1.0,
    .params  = {2.0},
    .tunable = {true},
};

static double blackman(const struct pl_filter_ctx *f, double x)
{
    double a = f->params[0];
    double a0 = (1 - a) / 2.0, a1 = 1 / 2.0, a2 = a / 2.0;
    x *= M_PI;
    return a0 + a1 * cos(x) + a2 * cos(2 * x);
}

const struct pl_filter_function pl_filter_function_blackman = {
    .name    = "blackman",
    .weight  = blackman,
    .radius  = 1.0,
    .params  = {0.16},
    .tunable = {true},
};

static double bohman(const struct pl_filter_ctx *f, double x)
{
    double pix = M_PI * x;
    return (1.0 - x) * cos(pix) + sin(pix) / M_PI;
}

const struct pl_filter_function pl_filter_function_bohman = {
    .name   = "bohman",
    .weight = bohman,
    .radius = 1.0,
};

static double gaussian(const struct pl_filter_ctx *f, double x)
{
    return exp(-2.0 * x * x / f->params[0]);
}

const struct pl_filter_function pl_filter_function_gaussian = {
    .name      = "gaussian",
    .weight    = gaussian,
    .radius    = 2.0,
    .resizable = true,
    .params    = {1.0},
    .tunable   = {true},
};

static double quadratic(const struct pl_filter_ctx *f, double x)
{
    if (x < 0.5) {
        return 1.0 - 4.0/3.0 * (x * x);
    } else {
        return 2.0 / 3.0 * (x - 1.5) * (x - 1.5);
    }
}

const struct pl_filter_function pl_filter_function_quadratic = {
    .name   = "quadratic",
    .weight = quadratic,
    .radius = 1.5,
};

static const struct pl_filter_function filter_function_quadric = {
    .name   = "quadric", // alias
    .weight = quadratic,
    .radius = 1.5,
};

static double sinc(const struct pl_filter_ctx *f, double x)
{
    if (x < 1e-8)
        return 1.0;
    x *= M_PI;
    return sin(x) / x;
}

const struct pl_filter_function pl_filter_function_sinc = {
    .name      = "sinc",
    .weight    = sinc,
    .radius    = 1.0,
    .resizable = true,
};

static double jinc(const struct pl_filter_ctx *f, double x)
{
    if (x < 1e-8)
        return 1.0;
    x *= M_PI;
    return 2.0 * j1(x) / x;
}

const struct pl_filter_function pl_filter_function_jinc = {
    .name      = "jinc",
    .weight    = jinc,
    .radius    = 1.2196698912665045, // first zero
    .resizable = true,
};

static double sphinx(const struct pl_filter_ctx *f, double x)
{
    if (x < 1e-8)
        return 1.0;
    x *= M_PI;
    return 3.0 * (sin(x) - x * cos(x)) / (x * x * x);
}

const struct pl_filter_function pl_filter_function_sphinx = {
    .name      = "sphinx",
    .weight    = sphinx,
    .radius    = 1.4302966531242027, // first zero
    .resizable = true,
};

static double cubic(const struct pl_filter_ctx *f, double x)
{
    const double b = f->params[0], c = f->params[1];
    double p0 = 6.0 - 2.0 * b,
           p2 = -18.0 + 12.0 * b + 6.0 * c,
           p3 = 12.0 - 9.0 * b - 6.0 * c,
           q0 = 8.0 * b + 24.0 * c,
           q1 = -12.0 * b - 48.0 * c,
           q2 = 6.0 * b + 30.0 * c,
           q3 = -b - 6.0 * c;

    if (x < 1.0) {
        return (p0 + x * x * (p2 + x * p3)) / p0;
    } else {
        return (q0 + x * (q1 + x * (q2 + x * q3))) / p0;
    }
}

const struct pl_filter_function pl_filter_function_cubic = {
    .name    = "cubic",
    .weight  = cubic,
    .radius  = 2.0,
    .params  = {1.0, 0.0},
    .tunable = {true, true},
};

const struct pl_filter_function pl_filter_function_hermite = {
    .name    = "hermite",
    .weight  = cubic,
    .radius  = 1.0,
    .params  = {0.0, 0.0},
};

const struct pl_filter_function pl_filter_function_bicubic = {
    .name    = "bicubic",
    .weight  = cubic,
    .radius  = 2.0,
    .params  = {1.0, 0.0},
    .tunable = {true, true},
};

const struct pl_filter_function pl_filter_function_bcspline = {
    .name    = "bcspline",
    .weight  = cubic,
    .radius  = 2.0,
    .params  = {1.0, 0.0},
    .tunable = {true, true},
};

const struct pl_filter_function pl_filter_function_catmull_rom = {
    .name    = "catmull_rom",
    .weight  = cubic,
    .radius  = 2.0,
    .params  = {0.0, 0.5},
    .tunable = {true, true},
};

const struct pl_filter_function pl_filter_function_mitchell = {
    .name    = "mitchell",
    .weight  = cubic,
    .radius  = 2.0,
    .params  = {1/3.0, 1/3.0},
    .tunable = {true, true},
};

const struct pl_filter_function pl_filter_function_robidoux = {
    .name    = "robidoux",
    .weight  = cubic,
    .radius  = 2.0,
    .params  = {12 / (19 + 9 * M_SQRT2), 113 / (58 + 216 * M_SQRT2)},
    .tunable = {true, true},
};

const struct pl_filter_function pl_filter_function_robidouxsharp = {
    .name    = "robidouxsharp",
    .weight  = cubic,
    .radius  = 2.0,
    .params  = {6 / (13 + 7 * M_SQRT2), 7 / (2 + 12 * M_SQRT2)},
    .tunable = {true, true},
};

static double spline16(const struct pl_filter_ctx *f, double x)
{
    if (x < 1.0) {
        return ((x - 9.0/5.0 ) * x - 1.0/5.0 ) * x + 1.0;
    } else {
        return ((-1.0/3.0 * (x-1) + 4.0/5.0) * (x-1) - 7.0/15.0 ) * (x-1);
    }
}

const struct pl_filter_function pl_filter_function_spline16 = {
    .name   = "spline16",
    .weight = spline16,
    .radius = 2.0,
};

static double spline36(const struct pl_filter_ctx *f, double x)
{
    if (x < 1.0) {
        return ((13.0/11.0 * x - 453.0/209.0) * x - 3.0/209.0) * x + 1.0;
    } else if (x < 2.0) {
        return ((-6.0/11.0 * (x-1) + 270.0/209.0) * (x-1) - 156.0/ 209.0) * (x-1);
    } else {
        return ((1.0/11.0 * (x-2) - 45.0/209.0) * (x-2) +  26.0/209.0) * (x-2);
    }
}

const struct pl_filter_function pl_filter_function_spline36 = {
    .name   = "spline36",
    .weight = spline36,
    .radius = 3.0,
};

static double spline64(const struct pl_filter_ctx *f, double x)
{
    if (x < 1.0) {
        return ((49.0/41.0 * x - 6387.0/2911.0) * x - 3.0/2911.0) * x + 1.0;
    } else if (x < 2.0) {
        return ((-24.0/41.0 * (x-1) + 4032.0/2911.0) * (x-1) - 2328.0/2911.0) * (x-1);
    } else if (x < 3.0) {
        return ((6.0/41.0 * (x-2) - 1008.0/2911.0) * (x-2) + 582.0/2911.0) * (x-2);
    } else {
        return ((-1.0/41.0 * (x-3) + 168.0/2911.0) * (x-3) - 97.0/2911.0) * (x-3);
    }
}

const struct pl_filter_function pl_filter_function_spline64 = {
    .name   = "spline64",
    .weight = spline64,
    .radius = 4.0,
};

static double oversample(const struct pl_filter_ctx *f, double x)
{
    return 0.0;
}

const struct pl_filter_function pl_filter_function_oversample = {
    .name    = "oversample",
    .weight  = oversample,
    .params  = {0.0},
    .tunable = {true},
    .opaque  = true,
};

const struct pl_filter_function * const pl_filter_functions[] = {
    &pl_filter_function_box,
    &filter_function_dirichlet, // alias
    &pl_filter_function_triangle,
    &pl_filter_function_cosine,
    &pl_filter_function_hann,
    &filter_function_hanning, // alias
    &pl_filter_function_hamming,
    &pl_filter_function_welch,
    &pl_filter_function_kaiser,
    &pl_filter_function_blackman,
    &pl_filter_function_bohman,
    &pl_filter_function_gaussian,
    &pl_filter_function_quadratic,
    &filter_function_quadric, // alias
    &pl_filter_function_sinc,
    &pl_filter_function_jinc,
    &pl_filter_function_sphinx,
    &pl_filter_function_cubic,
    &pl_filter_function_hermite,
    &pl_filter_function_bicubic,
    &pl_filter_function_bcspline,
    &pl_filter_function_catmull_rom,
    &pl_filter_function_mitchell,
    &pl_filter_function_robidoux,
    &pl_filter_function_robidouxsharp,
    &pl_filter_function_spline16,
    &pl_filter_function_spline36,
    &pl_filter_function_spline64,
    &pl_filter_function_oversample,
    NULL,
};

const int pl_num_filter_functions = PL_ARRAY_SIZE(pl_filter_functions) - 1;

const struct pl_filter_function *pl_find_filter_function(const char *name)
{
    if (!name)
        return NULL;

    for (int i = 0; i < pl_num_filter_functions; i++) {
        if (strcmp(name, pl_filter_functions[i]->name) == 0)
            return pl_filter_functions[i];
    }

    return NULL;
}

// Built-in filter function configs

const struct pl_filter_config pl_filter_spline16 = {
    .name        = "spline16",
    .description = "Spline (2 taps)",
    .kernel      = &pl_filter_function_spline16,
    .allowed     = PL_FILTER_ALL,
};

const struct pl_filter_config pl_filter_spline36 = {
    .name        = "spline36",
    .description = "Spline (3 taps)",
    .kernel      = &pl_filter_function_spline36,
    .allowed     = PL_FILTER_ALL,
};

const struct pl_filter_config pl_filter_spline64 = {
    .name        = "spline64",
    .description = "Spline (4 taps)",
    .kernel      = &pl_filter_function_spline64,
    .allowed     = PL_FILTER_ALL,
};

const struct pl_filter_config pl_filter_nearest = {
    .name        = "nearest",
    .description = "Nearest neighbor",
    .kernel      = &pl_filter_function_box,
    .radius      = 0.5,
    .allowed     = PL_FILTER_UPSCALING,
    .recommended = PL_FILTER_UPSCALING,
};

const struct pl_filter_config pl_filter_box = {
    .name        = "box",
    .description = "Box averaging",
    .kernel      = &pl_filter_function_box,
    .radius      = 0.5,
    .allowed     = PL_FILTER_SCALING,
    .recommended = PL_FILTER_DOWNSCALING,
};

const struct pl_filter_config pl_filter_bilinear = {
    .name        = "bilinear",
    .description = "Bilinear",
    .kernel      = &pl_filter_function_triangle,
    .allowed     = PL_FILTER_ALL,
    .recommended = PL_FILTER_SCALING,
};

const struct pl_filter_config filter_linear = {
    .name        = "linear",
    .description = "Linear mixing",
    .kernel      = &pl_filter_function_triangle,
    .allowed     = PL_FILTER_FRAME_MIXING,
    .recommended = PL_FILTER_FRAME_MIXING,
};

static const struct pl_filter_config filter_triangle = {
    .name        = "triangle",
    .kernel      = &pl_filter_function_triangle,
    .allowed     = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_gaussian = {
    .name        = "gaussian",
    .description = "Gaussian",
    .kernel      = &pl_filter_function_gaussian,
    .params      = {1.0},
    .allowed     = PL_FILTER_ALL,
    .recommended = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_sinc = {
    .name        = "sinc",
    .description = "Sinc (unwindowed)",
    .kernel      = &pl_filter_function_sinc,
    .radius      = 3.0,
    .allowed     = PL_FILTER_ALL,
};

const struct pl_filter_config pl_filter_lanczos = {
    .name        = "lanczos",
    .description = "Lanczos",
    .kernel      = &pl_filter_function_sinc,
    .window      = &pl_filter_function_sinc,
    .radius      = 3.0,
    .allowed     = PL_FILTER_ALL,
    .recommended = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_ginseng = {
    .name        = "ginseng",
    .description = "Ginseng (Jinc-Sinc)",
    .kernel      = &pl_filter_function_sinc,
    .window      = &pl_filter_function_jinc,
    .radius      = 3.0,
    .allowed     = PL_FILTER_ALL,
};

#define JINC_ZERO3 3.2383154841662362076499
#define JINC_ZERO4 4.2410628637960698819573

const struct pl_filter_config pl_filter_ewa_jinc = {
    .name        = "ewa_jinc",
    .description = "EWA Jinc (unwindowed)",
    .kernel      = &pl_filter_function_jinc,
    .radius      = JINC_ZERO3,
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_ewa_lanczos = {
    .name        = "ewa_lanczos",
    .description = "Jinc (EWA Lanczos)",
    .kernel      = &pl_filter_function_jinc,
    .window      = &pl_filter_function_jinc,
    .radius      = JINC_ZERO3,
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
    .recommended = PL_FILTER_UPSCALING,
};

const struct pl_filter_config pl_filter_ewa_lanczossharp = {
    .name        = "ewa_lanczossharp",
    .description = "Sharpened Jinc",
    .kernel      = &pl_filter_function_jinc,
    .window      = &pl_filter_function_jinc,
    .radius      = JINC_ZERO3,
    .blur        = 0.98125058372237073562493,
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
    .recommended = PL_FILTER_UPSCALING,
};

const struct pl_filter_config pl_filter_ewa_lanczos4sharpest = {
    .name        = "ewa_lanczos4sharpest",
    .description = "Sharpened Jinc-AR, 4 taps",
    .kernel      = &pl_filter_function_jinc,
    .window      = &pl_filter_function_jinc,
    .radius      = JINC_ZERO4,
    .blur        = 0.88451209326050047745788,
    .antiring    = 0.8,
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
    .recommended = PL_FILTER_UPSCALING,
};

const struct pl_filter_config pl_filter_ewa_ginseng = {
    .name        = "ewa_ginseng",
    .description = "EWA Ginseng",
    .kernel      = &pl_filter_function_jinc,
    .window      = &pl_filter_function_sinc,
    .radius      = JINC_ZERO3,
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_ewa_hann = {
    .name        = "ewa_hann",
    .description = "EWA Hann",
    .kernel      = &pl_filter_function_jinc,
    .window      = &pl_filter_function_hann,
    .radius      = JINC_ZERO3,
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
};

static const struct pl_filter_config filter_ewa_hanning = {
    .name        = "ewa_hanning",
    .kernel      = &pl_filter_function_jinc,
    .window      = &pl_filter_function_hann,
    .radius      = JINC_ZERO3,
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
};

// Spline family
const struct pl_filter_config pl_filter_bicubic = {
    .name        = "bicubic",
    .description = "Bicubic",
    .kernel      = &pl_filter_function_cubic,
    .params      = {1.0, 0.0},
    .allowed     = PL_FILTER_SCALING,
    .recommended = PL_FILTER_SCALING,
};

static const struct pl_filter_config filter_cubic = {
    .name        = "cubic",
    .description = "Cubic",
    .kernel      = &pl_filter_function_cubic,
    .params      = {1.0, 0.0},
    .allowed     = PL_FILTER_FRAME_MIXING,
};

const struct pl_filter_config pl_filter_hermite = {
    .name        = "hermite",
    .description = "Hermite",
    .kernel      = &pl_filter_function_hermite,
    .allowed     = PL_FILTER_ALL,
    .recommended = PL_FILTER_DOWNSCALING | PL_FILTER_FRAME_MIXING,
};

const struct pl_filter_config pl_filter_catmull_rom = {
    .name        = "catmull_rom",
    .description = "Catmull-Rom",
    .kernel      = &pl_filter_function_cubic,
    .params      = {0.0, 0.5},
    .allowed     = PL_FILTER_ALL,
    .recommended = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_mitchell = {
    .name        = "mitchell",
    .description = "Mitchell-Netravali",
    .kernel      = &pl_filter_function_cubic,
    .params      = {1/3.0, 1/3.0},
    .allowed     = PL_FILTER_ALL,
    .recommended = PL_FILTER_DOWNSCALING,
};

const struct pl_filter_config pl_filter_mitchell_clamp = {
    .name        = "mitchell_clamp",
    .description = "Mitchell (clamped)",
    .kernel      = &pl_filter_function_cubic,
    .params      = {1/3.0, 1/3.0},
    .clamp       = 1.0,
    .allowed     = PL_FILTER_ALL,
};

const struct pl_filter_config pl_filter_robidoux = {
    .name        = "robidoux",
    .description = "Robidoux",
    .kernel      = &pl_filter_function_cubic,
    .params      = {12 / (19 + 9 * M_SQRT2), 113 / (58 + 216 * M_SQRT2)},
    .allowed     = PL_FILTER_ALL,
};

const struct pl_filter_config pl_filter_robidouxsharp = {
    .name        = "robidouxsharp",
    .description = "RobidouxSharp",
    .kernel      = &pl_filter_function_cubic,
    .params      = {6 / (13 + 7 * M_SQRT2), 7 / (2 + 12 * M_SQRT2)},
    .allowed     = PL_FILTER_ALL,
};

const struct pl_filter_config pl_filter_ewa_robidoux = {
    .name        = "ewa_robidoux",
    .description = "EWA Robidoux",
    .kernel      = &pl_filter_function_cubic,
    .params      = {12 / (19 + 9 * M_SQRT2), 113 / (58 + 216 * M_SQRT2)},
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_ewa_robidouxsharp = {
    .name        = "ewa_robidouxsharp",
    .description = "EWA RobidouxSharp",
    .kernel      = &pl_filter_function_cubic,
    .params      = {6 / (13 + 7 * M_SQRT2), 7 / (2 + 12 * M_SQRT2)},
    .polar       = true,
    .allowed     = PL_FILTER_SCALING,
};

const struct pl_filter_config pl_filter_oversample = {
    .name        = "oversample",
    .description = "Oversampling",
    .kernel      = &pl_filter_function_oversample,
    .params      = {0.0},
    .allowed     = PL_FILTER_UPSCALING | PL_FILTER_FRAME_MIXING,
    .recommended = PL_FILTER_UPSCALING | PL_FILTER_FRAME_MIXING,
};

const struct pl_filter_config * const pl_filter_configs[] = {
    // Sorted roughly in terms of priority / relevance
    &pl_filter_bilinear,
    &filter_triangle, // alias
    &filter_linear, // pseudo-alias (frame mixing only)
    &pl_filter_nearest,
    &pl_filter_spline16,
    &pl_filter_spline36,
    &pl_filter_spline64,
    &pl_filter_lanczos,
    &pl_filter_ewa_lanczos,
    &pl_filter_ewa_lanczossharp,
    &pl_filter_ewa_lanczos4sharpest,
    &pl_filter_bicubic,
    &filter_cubic, // pseudo-alias (frame mixing only)
    &pl_filter_hermite,
    &pl_filter_gaussian,
    &pl_filter_oversample,
    &pl_filter_mitchell,
    &pl_filter_mitchell_clamp,
    &pl_filter_sinc,
    &pl_filter_ginseng,
    &pl_filter_ewa_jinc,
    &pl_filter_ewa_ginseng,
    &pl_filter_ewa_hann,
    &filter_ewa_hanning, // alias
    &pl_filter_catmull_rom,
    &pl_filter_robidoux,
    &pl_filter_robidouxsharp,
    &pl_filter_ewa_robidoux,
    &pl_filter_ewa_robidouxsharp,

    NULL,
};

const int pl_num_filter_configs = PL_ARRAY_SIZE(pl_filter_configs) - 1;

const struct pl_filter_config *
pl_find_filter_config(const char *name, enum pl_filter_usage usage)
{
    if (!name)
        return NULL;

    for (int i = 0; i < pl_num_filter_configs; i++) {
        if ((pl_filter_configs[i]->allowed & usage) != usage)
            continue;
        if (strcmp(name, pl_filter_configs[i]->name) == 0)
            return pl_filter_configs[i];
    }

    return NULL;
}

// Backwards compatibility with older API

const struct pl_filter_function_preset pl_filter_function_presets[] = {
    {"none",            NULL},
    {"box",             &pl_filter_function_box},
    {"dirichlet",       &filter_function_dirichlet}, // alias
    {"triangle",        &pl_filter_function_triangle},
    {"cosine",          &pl_filter_function_cosine},
    {"hann",            &pl_filter_function_hann},
    {"hanning",         &filter_function_hanning}, // alias
    {"hamming",         &pl_filter_function_hamming},
    {"welch",           &pl_filter_function_welch},
    {"kaiser",          &pl_filter_function_kaiser},
    {"blackman",        &pl_filter_function_blackman},
    {"bohman",          &pl_filter_function_bohman},
    {"gaussian",        &pl_filter_function_gaussian},
    {"quadratic",       &pl_filter_function_quadratic},
    {"quadric",         &filter_function_quadric}, // alias
    {"sinc",            &pl_filter_function_sinc},
    {"jinc",            &pl_filter_function_jinc},
    {"sphinx",          &pl_filter_function_sphinx},
    {"cubic",           &pl_filter_function_cubic},
    {"hermite",         &pl_filter_function_hermite},
    {"bicubic",         &pl_filter_function_bicubic},
    {"bcspline",        &pl_filter_function_bcspline},
    {"catmull_rom",     &pl_filter_function_catmull_rom}, // alias
    {"mitchell",        &pl_filter_function_mitchell},
    {"robidoux",        &pl_filter_function_robidoux},
    {"robidouxsharp",   &pl_filter_function_robidouxsharp},
    {"spline16",        &pl_filter_function_spline16},
    {"spline36",        &pl_filter_function_spline36},
    {"spline64",        &pl_filter_function_spline64},
    {0},
};

const int pl_num_filter_function_presets = PL_ARRAY_SIZE(pl_filter_function_presets) - 1;

const struct pl_filter_function_preset *pl_find_filter_function_preset(const char *name)
{
    if (!name)
        return NULL;

    for (int i = 0; pl_filter_function_presets[i].name; i++) {
        if (strcmp(pl_filter_function_presets[i].name, name) == 0)
            return &pl_filter_function_presets[i];
    }

    return NULL;
}

const struct pl_filter_preset *pl_find_filter_preset(const char *name)
{
    if (!name)
        return NULL;

    for (int i = 0; pl_filter_presets[i].name; i++) {
        if (strcmp(pl_filter_presets[i].name, name) == 0)
            return &pl_filter_presets[i];
    }

    return NULL;
}

const struct pl_filter_preset pl_filter_presets[] = {
    {"none",                NULL,                   "Built-in sampling"},
    COMMON_FILTER_PRESETS,
    {0}
};

const int pl_num_filter_presets = PL_ARRAY_SIZE(pl_filter_presets) - 1;
