/*
 * i965_vpp_avs.c - Adaptive Video Scaler (AVS) block
 *
 * Copyright (C) 2014 Intel Corporation
 *   Author: Gwenole Beauchesne <gwenole.beauchesne@intel.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sub license, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice (including the
 * next paragraph) shall be included in all copies or substantial portions
 * of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
 * IN NO EVENT SHALL PRECISION INSIGHT AND/OR ITS SUPPLIERS BE LIABLE FOR
 * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "sysdeps.h"
#include <math.h>
#include <va/va.h>
#include "i965_vpp_avs.h"

typedef void (*AVSGenCoeffsFunc)(float *coeffs, int num_coeffs, int phase,
                                 int num_phases, float f);

/* Initializes all coefficients to zero */
static void
avs_init_coeffs(float *coeffs, int num_coeffs)
{
#if defined(__STDC_IEC_559__) && (__STDC_IEC_559__ > 0)
    memset(coeffs, 0, num_coeffs * sizeof(*coeffs));
#else
    int i;

    for (i = 0; i < num_coeffs; i++)
        coeffs[i] = 0.0f;
#endif
}

/* Computes the sinc(x) function */
static float
avs_sinc(float x)
{
    if (x == 0.0f)
        return 1.0f;
    return sin(x * M_PI) / (x * M_PI);
}

/* Convolution kernel for linear interpolation */
static float
avs_kernel_linear(float x)
{
    const float abs_x = fabsf(x);

    return abs_x < 1.0f ? 1 - abs_x : 0.0f;
}

/* Convolution kernel for Lanczos-based interpolation */
static float
avs_kernel_lanczos(float x, float a)
{
    const float abs_x = fabsf(x);

    return abs_x < a ? avs_sinc(x) * avs_sinc(x / a) : 0.0f;
}

/* Truncates floating-point value towards an epsilon factor */
static inline float
avs_trunc_coeff(float x, float epsilon)
{
    return rintf(x / epsilon) * epsilon;
}

/* Normalize coefficients for one sample/direction */
static void
avs_normalize_coeffs_1(float *coeffs, int num_coeffs, float epsilon)
{
    float s, sum = 0.0;
    int i, c, r, r1;

    for (i = 0; i < num_coeffs; i++)
        sum += coeffs[i];

    if (sum < epsilon)
        return;

    s = 0.0;
    for (i = 0; i < num_coeffs; i++)
        s += (coeffs[i] = avs_trunc_coeff(coeffs[i] / sum, epsilon));

    /* Distribute the remaining bits, while allocating more to the center */
    c = num_coeffs / 2;
    c = c - (coeffs[c - 1] > coeffs[c]);

    r = (1.0f - s) / epsilon;
    r1 = r / 4;
    if (coeffs[c + 1] == 0.0f)
        coeffs[c] += r * epsilon;
    else {
        coeffs[c] += (r - 2 * r1) * epsilon;
        coeffs[c - 1] += r1 * epsilon;
        coeffs[c + 1] += r1 * epsilon;
    }
}

/* Normalize all coefficients so that their sum yields 1.0f */
static void
avs_normalize_coeffs(AVSCoeffs *coeffs, const AVSConfig *config)
{
    avs_normalize_coeffs_1(coeffs->y_k_h, config->num_luma_coeffs,
                           config->coeff_epsilon);
    avs_normalize_coeffs_1(coeffs->y_k_v, config->num_luma_coeffs,
                           config->coeff_epsilon);
    avs_normalize_coeffs_1(coeffs->uv_k_h, config->num_chroma_coeffs,
                           config->coeff_epsilon);
    avs_normalize_coeffs_1(coeffs->uv_k_v, config->num_chroma_coeffs,
                           config->coeff_epsilon);
}

/* Validate coefficients for one sample/direction */
static bool
avs_validate_coeffs_1(float *coeffs, int num_coeffs, const float *min_coeffs,
                      const float *max_coeffs)
{
    int i;

    for (i = 0; i < num_coeffs; i++) {
        if (coeffs[i] < min_coeffs[i] || coeffs[i] > max_coeffs[i])
            return false;
    }
    return true;
}

/* Validate coefficients wrt. the supplied range in config */
static bool
avs_validate_coeffs(AVSCoeffs *coeffs, const AVSConfig *config)
{
    const AVSCoeffs * const min_coeffs = &config->coeff_range.lower_bound;
    const AVSCoeffs * const max_coeffs = &config->coeff_range.upper_bound;

    return avs_validate_coeffs_1(coeffs->y_k_h, config->num_luma_coeffs,
                                 min_coeffs->y_k_h, max_coeffs->y_k_h) &&
           avs_validate_coeffs_1(coeffs->y_k_v, config->num_luma_coeffs,
                                 min_coeffs->y_k_v, max_coeffs->y_k_v) &&
           avs_validate_coeffs_1(coeffs->uv_k_h, config->num_chroma_coeffs,
                                 min_coeffs->uv_k_h, max_coeffs->uv_k_h) &&
           avs_validate_coeffs_1(coeffs->uv_k_v, config->num_chroma_coeffs,
                                 min_coeffs->uv_k_v, max_coeffs->uv_k_v);
}

/* Generate coefficients for default quality (bilinear) */
static void
avs_gen_coeffs_linear(float *coeffs, int num_coeffs, int phase, int num_phases,
                      float f)
{
    const int c = num_coeffs / 2 - 1;
    const float p = (float)phase / (num_phases * 2);

    avs_init_coeffs(coeffs, num_coeffs);
    coeffs[c] = avs_kernel_linear(p);
    coeffs[c + 1] = avs_kernel_linear(p - 1);
}

/* Generate coefficients for high quality (lanczos) */
static void
avs_gen_coeffs_lanczos(float *coeffs, int num_coeffs, int phase, int num_phases,
                       float f)
{
    const int c = num_coeffs / 2 - 1;
    const int l = num_coeffs > 4 ? 3 : 2;
    const float p = (float)phase / (num_phases * 2);
    int i;

    if (f > 1.0f)
        f = 1.0f;
    for (i = 0; i < num_coeffs; i++)
        coeffs[i] = avs_kernel_lanczos((i - (c + p)) * f, l);
}

/* Generate coefficients with the supplied scaler */
static bool
avs_gen_coeffs(AVSState *avs, float sx, float sy, AVSGenCoeffsFunc gen_coeffs)
{
    const AVSConfig * const config = avs->config;
    int i;

    for (i = 0; i <= config->num_phases; i++) {
        AVSCoeffs * const coeffs = &avs->coeffs[i];

        gen_coeffs(coeffs->y_k_h, config->num_luma_coeffs,
                   i, config->num_phases, sx);
        gen_coeffs(coeffs->uv_k_h, config->num_chroma_coeffs,
                   i, config->num_phases, sx);
        gen_coeffs(coeffs->y_k_v, config->num_luma_coeffs,
                   i, config->num_phases, sy);
        gen_coeffs(coeffs->uv_k_v, config->num_chroma_coeffs,
                   i, config->num_phases, sy);

        avs_normalize_coeffs(coeffs, config);
        if (!avs_validate_coeffs(coeffs, config))
            return false;
    }
    return true;
}

/* Initializes AVS state with the supplied configuration */
void
avs_init_state(AVSState *avs, const AVSConfig *config)
{
    avs->config = config;
    avs->flags = 0;
    avs->scale_x = 0.0f;
    avs->scale_y = 0.0f;
}

/* Checks whether the AVS scaling parameters changed */
static inline bool
avs_params_changed(AVSState *avs, float sx, float sy, uint32_t flags)
{
    if (avs->flags != flags)
        return true;

    if (flags >= VA_FILTER_SCALING_HQ) {
        if (avs->scale_x != sx || avs->scale_y != sy)
            return true;
    } else {
        if (avs->scale_x == 0.0f || avs->scale_y == 0.0f)
            return true;
    }
    return false;
}

/* Updates AVS coefficients for the supplied factors and quality level */
bool
avs_update_coefficients(AVSState *avs, float sx, float sy, uint32_t flags)
{
    AVSGenCoeffsFunc gen_coeffs;

    flags &= VA_FILTER_SCALING_MASK;
    if (!avs_params_changed(avs, sx, sy, flags))
        return true;

    switch (flags) {
    case VA_FILTER_SCALING_HQ:
        gen_coeffs = avs_gen_coeffs_lanczos;
        break;
    default:
        gen_coeffs = avs_gen_coeffs_linear;
        break;
    }
    if (!avs_gen_coeffs(avs, sx, sy, gen_coeffs)) {
        assert(0 && "invalid set of coefficients generated");
        return false;
    }

    avs->flags = flags;
    avs->scale_x = sx;
    avs->scale_y = sy;
    return true;
}
