// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * feConvolveMatrix filter primitive renderer
 *
 * Authors:
 *   Felipe Corrêa da Silva Sanches <juca@members.fsf.org>
 *   Jasper van de Gronde <th.v.d.gronde@hccnet.nl>
 *
 * Copyright (C) 2007,2009 authors
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include <vector>
#include "display/cairo-templates.h"
#include "display/cairo-utils.h"
#include "display/nr-filter-convolve-matrix.h"
#include "display/nr-filter-slot.h"
#include "display/nr-filter-units.h"
#include "display/nr-filter-utils.h"

namespace Inkscape {
namespace Filters {

FilterConvolveMatrix::FilterConvolveMatrix() = default;

FilterConvolveMatrix::~FilterConvolveMatrix() = default;

enum PreserveAlphaMode
{
    PRESERVE_ALPHA,
    NO_PRESERVE_ALPHA
};

template <PreserveAlphaMode preserve_alpha>
struct ConvolveMatrix : public SurfaceSynth
{
    ConvolveMatrix(cairo_surface_t *s, int targetX, int targetY, int orderX, int orderY,
                   double divisor, double bias, std::vector<double> const &kernel)
        : SurfaceSynth(s)
        , _kernel(kernel.size())
        , _targetX(targetX)
        , _targetY(targetY)
        , _orderX(orderX)
        , _orderY(orderY)
        , _bias(bias)
    {
        for (unsigned i = 0; i < _kernel.size(); ++i) {
            _kernel[i] = kernel[i] / divisor;
        }
        // the matrix is given rotated 180 degrees
        // which corresponds to reverse element order
        std::reverse(_kernel.begin(), _kernel.end());
    }

    guint32 operator()(int x, int y) const
    {
        int startx = std::max(0, x - _targetX);
        int starty = std::max(0, y - _targetY);
        int endx = std::min(_w, startx + _orderX);
        int endy = std::min(_h, starty + _orderY);
        int limitx = endx - startx;
        int limity = endy - starty;
        double suma = 0.0, sumr = 0.0, sumg = 0.0, sumb = 0.0;

        for (int i = 0; i < limity; ++i) {
            for (int j = 0; j < limitx; ++j) {
                guint32 px = pixelAt(startx + j, starty + i);
                double coeff = _kernel[i * _orderX + j];
                EXTRACT_ARGB32(px, a,r,g,b)

                sumr += r * coeff;
                sumg += g * coeff;
                sumb += b * coeff;
                if (preserve_alpha == NO_PRESERVE_ALPHA) {
                    suma += a * coeff;
                }
            }
        }
        if (preserve_alpha == PRESERVE_ALPHA) {
            suma = alphaAt(x, y);
        } else {
            suma += _bias * 255;
        }

        guint32 ao = pxclamp(round(suma), 0, 255);
        guint32 ro = pxclamp(round(sumr + ao * _bias), 0, ao);
        guint32 go = pxclamp(round(sumg + ao * _bias), 0, ao);
        guint32 bo = pxclamp(round(sumb + ao * _bias), 0, ao);
        ASSEMBLE_ARGB32(pxout, ao,ro,go,bo);
        return pxout;
    }

private:
    std::vector<double> _kernel;
    int _targetX, _targetY, _orderX, _orderY;
    double _bias;
};

void FilterConvolveMatrix::render_cairo(FilterSlot &slot) const
{
    static bool bias_warning = false;
    static bool edge_warning = false;

    if (orderX<=0 || orderY<=0) {
        g_warning("Empty kernel!");
        return;
    }
    if (targetX<0 || targetX>=orderX || targetY<0 || targetY>=orderY) {
        g_warning("Invalid target!");
        return;
    }
    if (kernelMatrix.size()!=(unsigned int)(orderX*orderY)) {
        //g_warning("kernelMatrix does not have orderX*orderY elements!");
        return;
    }

    cairo_surface_t *input = slot.getcairo(_input);
    cairo_surface_t *out = ink_cairo_surface_create_identical(input);

    // We may need to transform input surface to correct color interpolation space. The input surface
    // might be used as input to another primitive but it is likely that all the primitives in a given
    // filter use the same color interpolation space so we don't copy the input before converting.
    set_cairo_surface_ci(out, color_interpolation);
    set_cairo_surface_ci(input, color_interpolation);

    if (bias != 0 && !bias_warning) {
        g_warning("It is unknown whether Inkscape's implementation of bias in feConvolveMatrix is correct!");
        bias_warning = true;
        // The SVG specification implies that feConvolveMatrix is defined for premultiplied
        // colors (which makes sense). It also says that bias should simply be added to the result
        // for each color (without taking the alpha into account). However, it also says that one
        // purpose of bias is "to have .5 gray value be the zero response of the filter".
        // It seems sensible to indeed support the latter behaviour instead of the former,
        // but this does appear to go against the standard.
        // Note that Batik simply does not support bias!=0
    }
    if (edgeMode != CONVOLVEMATRIX_EDGEMODE_NONE && !edge_warning) {
        g_warning("Inkscape only supports edgeMode=\"none\" (and a filter uses a different one)!");
        edge_warning = true;
    }

    //guint32 *in_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(input));
    //guint32 *out_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(out));

    //int width = cairo_image_surface_get_width(input);
    //int height = cairo_image_surface_get_height(input);

    // Set up predivided kernel matrix
    /*std::vector<double> kernel(kernelMatrix);
    for(size_t i=0; i<kernel.size(); i++) {
        kernel[i] /= divisor; // The code that creates this object makes sure that divisor != 0
    }*/

    if (preserveAlpha) {
        //convolve2D<true>(out_data, in_data, width, height, &kernel.front(), orderX, orderY,
        //    targetX, targetY, bias);
        ink_cairo_surface_synthesize(out, ConvolveMatrix<PRESERVE_ALPHA>(input,
            targetX, targetY, orderX, orderY, divisor, bias, kernelMatrix));
    } else {
        //convolve2D<false>(out_data, in_data, width, height, &kernel.front(), orderX, orderY,
        //    targetX, targetY, bias);
        ink_cairo_surface_synthesize(out, ConvolveMatrix<NO_PRESERVE_ALPHA>(input,
            targetX, targetY, orderX, orderY, divisor, bias, kernelMatrix));
    }

    slot.set(_output, out);
    cairo_surface_destroy(out);
}

void FilterConvolveMatrix::set_targetX(int coord)
{
    targetX = coord;
}

void FilterConvolveMatrix::set_targetY(int coord)
{
    targetY = coord;
}

void FilterConvolveMatrix::set_orderX(int coord)
{
    orderX = coord;
}

void FilterConvolveMatrix::set_orderY(int coord)
{
    orderY = coord;
}

void FilterConvolveMatrix::set_divisor(double d)
{
    divisor = d;
}

void FilterConvolveMatrix::set_bias(double b)
{
    bias = b;
}

void FilterConvolveMatrix::set_kernelMatrix(std::vector<gdouble> km)
{
    kernelMatrix = std::move(km);
}

void FilterConvolveMatrix::set_edgeMode(FilterConvolveMatrixEdgeMode mode)
{
    edgeMode = mode;
}

void FilterConvolveMatrix::set_preserveAlpha(bool pa)
{
    preserveAlpha = pa;
}

void FilterConvolveMatrix::area_enlarge(Geom::IntRect &area, Geom::Affine const &/*trans*/) const
{
    //Seems to me that since this filter's operation is resolution dependent,
    // some spurious pixels may still appear at the borders when low zooming or rotating. Needs a better fix.
    area.setMin(area.min() - Geom::IntPoint(targetX, targetY));
    // This makes sure the last row/column in the original image corresponds
    // to the last row/column in the new image that can be convolved without
    // adjusting the boundary conditions).
    area.setMax(area.max() + Geom::IntPoint(orderX - targetX - 1, orderY - targetY - 1));
}

double FilterConvolveMatrix::complexity(Geom::Affine const &) const
{
    return kernelMatrix.size();
}

} // namespace Filters
} // namespace Inkscape

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
