/*
 *  $Id: level.c 29544 2026-02-25 10:28:00Z yeti-dn $
 *  Copyright (C) 2003-2026 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program 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 General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/field.h"
#include "libgwyddion/level.h"

#include "libgwyddion/omp.h"
#include "libgwyddion/internal.h"

static gint
fit_plane_nomask(GwyField *field,
                 gint col, gint row, gint width, gint height,
                 gdouble *pa, gdouble *pbx, gdouble *pby)
{
    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;

    /* NB: These are actually mean values, not sums. */
    gdouble sx = (width - 1.0)/2.0;
    gdouble sxx = (width*width - 1.0)/12.0;
    gdouble sy = (height - 1.0)/2.0;
    gdouble syy = (height*height - 1.0)/12.0;
    gdouble sz = 0.0, sxz = 0.0, syz = 0.0;
    gdouble xc = sx, yc = sy;
    gdouble n = width*height;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        reduction(+:sz,sxz,syz) \
        shared(datapos,xres,width,height,xc,yc)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        gdouble y = i - yc;
        for (gint j = 0; j < width; j++) {
            gdouble x = j - xc, z = drow[j];
            sz += z;
            sxz += x*z;
            syz += y*z;
        }
    }

    gint retval = 1;
    gdouble a = sz/n, bx = 0.0, by = 0.0;
    if (width > 1) {
        bx = sxz/(n*sxx);
        retval++;
    }
    if (height > 1) {
        by = syz/(n*syy);
        retval++;
    }
    a -= bx*xc + by*yc;

    if (pbx)
        *pbx = bx;
    if (pby)
        *pby = by;
    if (pa)
        *pa = a;

    return retval;
}

/**
 * gwy_field_fit_plane:
 * @field: A data field.
 * @a: (out) (optional): Where constant coefficient should be stored (or %NULL).
 * @bx: (out) (optional): Where x plane coefficient should be stored (or %NULL).
 * @by: (out) (optional): Where y plane coefficient should be stored (or %NULL).
 *
 * Fits a plane through a data field.
 *
 * The coefficients can be used for plane leveling using relation data[i,j] → data[i,j] - (a + by*i + bx*j), where
 * the integer indices start from (0, 0) at the image top left corner.
 *
 * Returns: The rank of the normal matrix.
 **/
gint
gwy_field_fit_plane(GwyField *field,
                    gdouble *pa, gdouble *pbx, gdouble *pby)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0);
    return fit_plane_nomask(field, 0, 0, field->xres, field->yres, pa, pbx, pby);
}

/**
 * gwy_field_area_fit_plane:
 * @field: A data field
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @a: (out) (optional): Where constant coefficient should be stored (or %NULL).
 * @bx: (out) (optional): Where x plane coefficient should be stored (or %NULL).
 * @by: (out) (optional): Where y plane coefficient should be stored (or %NULL).
 *
 * Fits a plane through a rectangular part of a data field with masking.
 *
 * The coefficients can be used for plane leveling using relation data[i,j] → data[i,j] - (a + by*i + bx*j), where
 * the integer indices start from (0, 0) at the top left corner of the are (not the image).
 *
 * If the returned value is smaller than 3 then the pixel configuration is degenerate in some sense, for instance
 * there are only two pixels contributing to the plane fitting. The function calculates the minimum norm solution.
 * This is reasonable for instance for 1×N areas where it gives a zero coefficient for the undetermined direction and
 * line coefficients for the good direction. However, one may also simply not use the result in degenerate cases.
 *
 * Returns: The rank of the normal matrix.
 **/
gint
gwy_field_area_fit_plane(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gint col, gint row,
                         gint width, gint height,
                         gdouble *pa, gdouble *pbx, gdouble *pby)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0;

    if (!mask)
        return fit_plane_nomask(field, col, row, width, height, pa, pbx, pby);

    guint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *maskpos = mask->priv->data + row*xres + col;
    gdouble sx = 0.0, sy = 0.0, sz = 0.0;
    gint retval = 0;

    guint n = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
    reduction(+:n,sx,sy,sz) \
    shared(datapos,maskpos,xres,width,height,masking)
#endif
    for (guint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;
        for (guint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking)) {
                gdouble z = drow[j];
                sx += j;
                sy += i;
                sz += z;
                n++;
            }
        }
    }

    gdouble a = 0.0, bx = 0.0, by = 0.0;
    gdouble xc = sx/n, yc = sy/n;

    if (n == 0) {
        /* Keep everything as zeros. */
        goto end;
    }

    a = sz/n;
    if (n == 1) {
        /* Single pixel. The minimum-norm solution is zero plane coefficients.*/
        retval = 1;
        goto end;
    }
    /* Distinguishing n=2 is not useful because the degenerate case ‘pixels in a straight line’ has many possible
     * confiurations. Handle it generically below. */

    gdouble sxz = 0.0, syz = 0.0, sxx = 0.0, syy = 0.0, sxy = 0.0;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
    reduction(+:sxx,syy,sxy,sxz,syz) \
    shared(datapos,maskpos,xres,width,height,xc,yc,masking)
#endif
    for (guint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;
        gdouble y = i - yc;
        for (guint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking)) {
                gdouble x = j - xc, z = drow[j];
                sxx += x*x;
                syy += y*y;
                sxy += x*y;
                sxz += x*z;
                syz += y*z;
            }
        }
    }

    retval = gwy_math_plane_coefficients(sxx, sxy, syy, sxz, syz, n, &bx, &by) + 1;

end:
    a -= bx*xc + by*yc;
    if (pbx)
        *pbx = bx;
    if (pby)
        *pby = by;
    if (pa)
        *pa = a;

    return retval;
}

/**
 * gwy_field_fit_facet_plane:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.
 * @a: (out) (optional): Where constant coefficient should be stored (or %NULL).
 * @bx: (out): Where x plane coefficient should be stored.
 * @by: (out): Where y plane coefficient should be stored.
 *
 * Calculates the inclination of a plane close to the dominant plane in a data field.
 *
 * The dominant plane is determined by taking into account larger local slopes with exponentially smaller weight.
 *
 * This is the basis of so-called facet levelling algorithm.  Usually, the plane found by this method is subtracted
 * using gwy_field_plane_level() and the entire process is repeated until it converges.  A convergence criterion
 * may be sufficiently small values of the x and y plane coefficients.  Note that since gwy_field_plane_level()
 * uses pixel-based lateral coordinates, the coefficients must be divided by gwy_field_get_dx(field) and
 * gwy_field_get_dy(field) to obtain physical plane coefficients.
 *
 * Returns: %TRUE if any plane was actually fitted; %FALSE if there was an insufficient number of unmasked pixels.
 **/
gboolean
gwy_field_fit_facet_plane(GwyField *field,
                          GwyNield *mask,
                          GwyMaskingType masking,
                          gdouble *pa, gdouble *pbx, gdouble *pby)
{
    if (!_gwy_field_check_mask(field, &mask, &masking))
        return FALSE;

    const gdouble c = 1.0/20.0;

    *pbx = *pby = 0.0;
    if (pa)
        *pa = 0.0;

    gint xres = field->xres, yres = field->yres;
    gdouble dx = field->xreal/xres, dy = field->yreal/yres;

    const gdouble *data = field->priv->data;
    const gint *mdata = mask ? mask->priv->data : NULL;

    /* Normalisation coefficient, i.e. baseline magnitude of the normals. */
    gdouble sigma2 = 0.0;
    gsize n = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        reduction(+:sigma2,n) \
        shared(data,mdata,xres,yres,dx,dy,masking)
#endif
    for (gint i = 1; i < yres; i++) {
        const gdouble *row = data + (i-1)*xres;
        const gdouble *row2 = data + i*xres;

        if (mdata) {
            const gint *mrow = mdata + (i-1)*xres;
            const gint *mrow2 = mdata + i*xres;

            for (gint j = 1; j < xres; j++) {
                if (nielded_included(mrow + j-1, masking) && nielded_included(mrow + j, masking)
                    && nielded_included(mrow2 + j-1, masking) && nielded_included(mrow2 + j, masking)) {
                    gdouble vx = 0.5*(row2[j] + row[j] - row2[j-1] - row[j-1])/dx;
                    gdouble vy = 0.5*(row2[j-1] + row2[j] - row[j-1] - row[j])/dy;
                    sigma2 += vx*vx + vy*vy;
                    n++;
                }
            }
        }
        else {
            for (gint j = 1; j < xres; j++) {
                gdouble vx = 0.5*(row2[j] + row[j] - row2[j-1] - row[j-1])/dx;
                gdouble vy = 0.5*(row2[j-1] + row2[j] - row[j-1] - row[j])/dy;
                sigma2 += vx*vx + vy*vy;
            }
            n += xres-1;
        }
    }
    /* Do not try to level from some random pixel */
    gwy_debug("n=%d", n);
    if (n < 4)
        return FALSE;

    sigma2 = c*sigma2/n;

    gdouble sumvx = 0.0, sumvy = 0.0, sumvz = 0.0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        reduction(+:sumvx,sumvy,sumvz) \
        shared(data,mdata,xres,yres,dx,dy,masking,sigma2)
#endif
    for (gint i = 1; i < yres; i++) {
        const gdouble *row = data + (i-1)*xres;
        const gdouble *row2 = data + i*xres;

        if (mdata) {
            const gint *mrow = mdata + (i-1)*xres;
            const gint *mrow2 = mdata + i*xres;

            for (gint j = 1; j < xres; j++) {
                if (nielded_included(mrow + j-1, masking) && nielded_included(mrow + j, masking)
                    && nielded_included(mrow2 + j-1, masking) && nielded_included(mrow2 + j, masking)) {
                    gdouble vx = 0.5*(row2[j] + row[j] - row2[j-1] - row[j-1])/dx;
                    gdouble vy = 0.5*(row2[j-1] + row2[j] - row[j-1] - row[j])/dy;
                    gdouble q = exp(-(vx*vx + vy*vy)/sigma2);
                    sumvx += vx*q;
                    sumvy += vy*q;
                    sumvz += q;
                }
            }
        }
        else {
            for (gint j = 1; j < xres; j++) {
                gdouble vx = 0.5*(row2[j] + row[j] - row2[j-1] - row[j-1])/dx;
                gdouble vy = 0.5*(row2[j-1] + row2[j] - row[j-1] - row[j])/dy;
                gdouble q = exp(-(vx*vx + vy*vy)/sigma2);
                sumvx += vx*q;
                sumvy += vy*q;
                sumvz += q;
            }
        }
    }

    gdouble q = sumvz;
    *pbx = sumvx/q * dx;
    *pby = sumvy/q * dy;
    gwy_debug("beta=%g, sigma=%g sum=(%g, %g) q=%g b=(%g, %g)",
              sqrt(sigma2/c), sqrt(sigma2), sumvx, sumvy, q, *pbx, *pby);

    if (pa)
        *pa = -0.5*((*pbx)*xres + (*pby)*yres);

    return TRUE;
}

/**
 * gwy_field_facet_level:
 * @field: A data field. It the procedure is cancelled it will be unchanged.
 * @mask: (nullable): Mask field specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.
 * @maxiter: Maximum number of levelling iterations. Pass a non-positive number for (a high) default.
 * @set_fraction: (scope call) (nullable): Function that sets progress fraction to output (or %NULL).
 *
 * Performs facet-levelling of a data field.
 *
 * The levelling consists of repeated computation of the dominant plane using gwy_field_fit_facet_plane() and
 * subtraction of the dominant plane. Usually only several iterations are required for convergence.
 *
 * Returns: %TRUE if levelling has finished (either by converging or reaching the maximum number of iterations);
 *          %FALSE if it was cancelled or there was an insufficient number of unmasked pixels.
 **/
gboolean
gwy_field_facet_level(GwyField *field,
                      GwyNield *mask,
                      GwyMaskingType masking,
                      gint maxiter,
                      GwySetFractionFunc set_fraction)
{
    if (!_gwy_field_check_mask(field, &mask, &masking))
        return FALSE;

    if (maxiter <= 0)
        maxiter = 100;

    GwyField *result = gwy_field_copy(field);
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    gdouble maxb2 = 666.0, eps = 1e-9;
    gdouble progress = 0.0;
    gint i;

    for (i = 0; i < maxiter; i++) {
        gdouble bx, by, c;
        if (!gwy_field_fit_facet_plane(result, mask, masking, &c, &bx, &by))
            break;
        gwy_field_plane_level(result, c, bx, by);
        bx /= dx;
        by /= dy;
        gdouble b2 = bx*bx + by*by;

        if (!i)
            maxb2 = MAX(b2, eps);
        if (b2 < eps) {
            i = maxiter;
            break;
        }
        if (set_fraction) {
            gdouble p = log(b2/maxb2)/log(eps/maxb2);
            gwy_debug("progress = %f, p = %f, ip = %f", progress, p, i/100.0);
            /* Never decrease progress, that would look silly. */
            progress = MAX(progress, p);
            progress = MAX(progress, i/100.0);
            if (!set_fraction(progress))
                break;
        }
    }

    if (i == maxiter)
        gwy_field_copy_data(result, field);

    g_object_unref(result);

    return i == maxiter;
}

/**
 * gwy_field_plane_level:
 * @field: A data field.
 * @a: Constant coefficient.
 * @bx: X plane coefficient.
 * @by: Y plane coefficient.
 *
 * Subtracts plane from a data field.
 *
 * See gwy_field_fit_plane() for details.
 **/
void
gwy_field_plane_level(GwyField *field,
                      gdouble a, gdouble bx, gdouble by)
{
    g_return_if_fail(GWY_IS_FIELD(field));

    for (gint i = 0; i < field->yres; i++) {
        gdouble *row = field->priv->data + i*field->xres;
        gdouble rb = a + by*i;

        for (gint j = 0; j < field->xres; j++)
            row[j] -= rb + bx*j;
    }

    gwy_field_invalidate(field);
}

/**
 * gwy_field_plane_rotate:
 * @field: A data field.
 * @xangle: Rotation angle in x direction (rotation along y axis, in radians).
 * @yangle: Rotation angle in y direction (rotation along x axis, in radians).
 * @interpolation: Interpolation type (can be only of two-point type).
 *
 * Performs rotation of plane along x and y axis.
 **/
void
gwy_field_plane_rotate(GwyField *field,
                       gdouble xangle,
                       gdouble yangle,
                       GwyInterpolationType interpolation)
{
    int k;
    GwyLine *l;

    g_return_if_fail(GWY_IS_FIELD(field));

    if (xangle != 0) {
        l = gwy_line_new(field->xres, field->xreal, FALSE);
        for (k = 0; k < field->yres; k++) {
            gwy_field_get_row(field, l, k);
            gwy_line_rotate(l, -xangle, interpolation);
            gwy_field_set_row(field, l, k);
        }
        g_object_unref(l);
    }

    if (yangle != 0) {
        l = gwy_line_new(field->yres, field->yreal, FALSE);
        for (k = 0; k < field->xres; k++) {
            gwy_field_get_column(field, l, k);
            gwy_line_rotate(l, -yangle, interpolation);
            gwy_field_set_column(field, l, k);
        }
        g_object_unref(l);
    }

    gwy_field_invalidate(field);
}

#if 0
void
gwy_field_plane_true_rotate(GwyField *field,
                            gdouble xangle,
                            gdouble yangle,
                            GwyInterpolationType interpolation)
{
    gdouble diag, dx, dy, phi, phi0, theta, tx, ty;
    gint xres, yres, txres, tyres, xbw, ybw, i;
    gdouble *data, *tdata;
    GwyField *tmp;

    if (xangle == 0 || yangle == 0) {
        gwy_field_plane_rotate(field, xangle, yangle, interpolation);
        return;
    }

    xres = field->xres;
    yres = field->yres;
    data = field->priv->data;

    dx = tan(xangle);
    dy = tan(yangle);
    phi = atan2(dy, dx);
    theta = atan(hypot(dx, dy));
    phi0 = atan2(yres, xres);
    diag = hypot(xres, yres);
    tx = MAX(fabs(cos(-phi + phi0)), fabs(cos(-phi - phi0)));
    ty = MAX(fabs(sin(-phi + phi0)), fabs(sin(-phi - phi0)));
    txres = ((guint)GWY_ROUND(diag*tx + 2));
    tyres = ((guint)GWY_ROUND(diag*ty + 2));
    /* Keep parity to make the rotation less fuzzy */
    xbw = (txres - xres + 1)/2;
    if (xres + 2*xbw != txres)
        txres++;
    ybw = (tyres - yres + 1)/2;
    if (yres + 2*ybw != tyres)
        tyres++;

    /* Rotate to a temporary data field extended with border pixels */
    tmp = gwy_field_new(txres, tyres, 1.0, 1.0, FALSE);
    tdata = tmp->priv->data;
    /* Copy */
    gwy_field_area_copy(field, tmp, 0, 0, xres, yres, xbw, ybw);
    /* Corners */
    gwy_field_area_fill(tmp, NULL, GWY_MASK_IGNORE, 0, 0, xbw, ybw,
                        data[0]);
    gwy_field_area_fill(tmp, NULL, GWY_MASK_IGNORE, xres + xbw, 0, xbw, ybw,
                        data[xres-1]);
    gwy_field_area_fill(tmp, NULL, GWY_MASK_IGNORE, 0, yres + ybw, xbw, ybw,
                             data[xres*(yres - 1)]);
    gwy_field_area_fill(tmp, NULL, GWY_MASK_IGNORE, xres + xbw, yres + ybw, xbw, ybw,
                        data[xres*yres - 1]);
    /* Sides */
    for (i = 0; i < ybw; i++)
        memcpy(tdata + i*txres + xbw, data,
               xres*sizeof(gdouble));
    for (i = 0; i < ybw; i++)
        memcpy(tdata + (yres + ybw + i)*txres + xbw, data + xres*(yres - 1),
               xres*sizeof(gdouble));
    for (i = 0; i < yres; i++) {
        gwy_field_area_fill(tmp, NULL, GWY_MASK_IGNORE, 0, ybw + i, xbw, 1,
                            data[i*xres]);
        gwy_field_area_fill(tmp, NULL, GWY_MASK_IGNORE, xres + xbw, ybw + i, xbw, 1,
                            data[i*xres + xres - 1]);
    }

    /* Rotate in xy to make the space rotation along y axis */
    gwy_field_rotate(tmp, -phi, interpolation);
    /* XXX: Still, individual gwy_line_rotate() can resample differently,
     * causing incompatible rows in the image.  And we cannot get the
     * resampling information from gwy_line_rotate(). */
    gwy_field_plane_rotate(tmp, theta, 0, GWY_INTERPOLATION_LINEAR);
    /* TODO:
     * recalculate xres
     * make samples square again
     */
    gwy_field_rotate(tmp, phi, interpolation);
    /* XXX: xbw is no longer correct border */
    gwy_field_area_copy(tmp, field, xbw, ybw, xres, yres, 0, 0);

    g_object_unref(tmp);
}
#endif

/* Calculate values of Legendre polynomials from 0 to @n in @x. */
static void
legendre_all(gdouble x,
             guint n,
             gdouble *p)
{
    guint m;

    p[0] = 1.0;
    if (n == 0)
        return;
    p[1] = x;
    if (n == 1)
        return;

    for (m = 2; m <= n; m++)
        p[m] = (x*(2*m - 1)*p[m-1] - (m - 1)*p[m-2])/m;
}

/**
 * gwy_field_area_fit_legendre:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @col_degree: Degree of polynomial to fit column-wise (x-coordinate).
 * @row_degree: Degree of polynomial to fit row-wise (y-coordinate).
 * @coeffs: (nullable):
 *          An array of size (@row_degree+1)*(@col_degree+1) to store the coefficients to, or %NULL (a fresh array is
 *          allocated then).
 *
 * Fits two-dimensional Legendre polynomial to a rectangular part of a data field.
 *
 * The @col_degree and @row_degree parameters limit the maximum powers of x and y exactly as if simple powers were
 * fitted, therefore if you do not intend to interpret contents of @coeffs youself, the only difference is that this
 * method is much more numerically stable.
 *
 * The coefficients are organized exactly like in gwy_field_area_fit_poly(), but they are not coefficients of
 * x^n y^m, instead they are coefficients of P_n(x) P_m(x), where P are Legendre polynomials.  The polynomials are
 * evaluated in coordinates where first row (column) corresponds to -1.0, and the last row (column) to 1.0.
 *
 * Note the polynomials are normal Legendre polynomials that are not exactly orthogonal on a discrete point set (if
 * their degrees are equal mod 2).
 *
 * Returns: Either @coeffs if it was not %NULL, or a newly allocated array with coefficients.
 **/
gdouble*
gwy_field_area_fit_legendre(GwyField *field,
                            gint col, gint row,
                            gint width, gint height,
                            gint col_degree, gint row_degree,
                            gdouble *coeffs)
{
    gint r, c, i, j, size, maxsize, xres, col_n, row_n;
    gint isize, jsize, thissize;
    gdouble *data, *m, *pmx, *pmy, *sumsx, *sumsy, *rhs;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return NULL;
    g_return_val_if_fail(row_degree >= 0 && col_degree >= 0, NULL);
    xres = field->xres;
    data = field->priv->data;
    col_n = col_degree + 1;
    row_n = row_degree + 1;

    size = col_n*row_n;
    /* The maximum necessary matrix size (order), it is approximately four times smaller thanks to separation of even
     * and odd polynomials */
    maxsize = ((col_n + 1)/2)*((row_n + 1)/2);
    if (!coeffs)
        coeffs = g_new0(gdouble, size);
    else
        gwy_clear(coeffs, size);

    sumsx = g_new0(gdouble, col_n*col_n);
    sumsy = g_new0(gdouble, row_n*row_n);
    rhs = g_new(gdouble, maxsize);
    m = g_new(gdouble, MAX(maxsize*(maxsize + 1)/2, col_n + row_n));
    /* pmx, pmy and m are not needed at the same time, reuse it */
    pmx = m;
    pmy = m + col_n;

    /* Calculate <P_m(x) P_n(y) z(x,y)> (normalized to complete area) */
    for (r = 0; r < height; r++) {
        legendre_all(2*r/(height - 1.0) - 1.0, row_degree, pmy);
        for (c = 0; c < width; c++) {
            gdouble z = data[(row + r)*xres + (col + c)];

            legendre_all(2*c/(width - 1.0) - 1.0, col_degree, pmx);
            for (i = 0; i < row_n; i++) {
                for (j = 0; j < col_n; j++)
                    coeffs[i*col_n + j] += z*pmx[j]*pmy[i];
            }
        }
    }

    /* Calculate <P_m(x) P_a(x)> (normalized to single row). 3/4 of these values are zeroes, but it only takes
     * O(width) time. */
    for (c = 0; c < width; c++) {
        legendre_all(2*c/(width - 1.0) - 1.0, col_degree, pmx);
        for (i = 0; i < col_n; i++) {
            for (j = 0; j < col_n; j++)
                sumsx[i*col_n + j] += pmx[i]*pmx[j];
        }
    }

    /* Calculate <P_n(y) P_b(y)> (normalized to single column) 3/4 of these values are zeroes, but it only takes
     * O(height) time. */
    for (r = 0; r < height; r++) {
        legendre_all(2*r/(height - 1.0) - 1.0, row_degree, pmy);
        for (i = 0; i < row_n; i++) {
            for (j = 0; j < row_n; j++)
                sumsy[i*row_n + j] += pmy[i]*pmy[j];
        }
    }

    /* (Even, Even) */
    isize = (row_n + 1)/2;
    jsize = (col_n + 1)/2;
    thissize = jsize*isize;
    /* This is always true */
    if (thissize) {
        /* Construct the submatrix */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize);
            gint iy = 2*(i/jsize);
            gdouble *mrow = m + i*(i + 1)/2;

            for (j = 0; j <= i; j++) {
                gint jx = 2*(j % jsize);
                gint jy = 2*(j/jsize);

                mrow[j] = sumsx[ix*col_n + jx]*sumsy[iy*row_n + jy];
            }
        }
        /* Construct the subrhs */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize);
            gint iy = 2*(i/jsize);

            rhs[i] = coeffs[iy*col_n + ix];
        }
        /* Solve */
        if (!gwy_math_choleski_decompose(thissize, m)) {
            gwy_clear(coeffs, size);
            goto fail;
        }
        gwy_math_choleski_solve(thissize, m, rhs);
        /* Copy back */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize);
            gint iy = 2*(i/jsize);

            coeffs[iy*col_n + ix] = rhs[i];
        }
    }

    /* (Even, Odd) */
    isize = (row_n + 1)/2;
    jsize = col_n/2;
    thissize = jsize*isize;
    if (thissize) {
        /* Construct the submatrix */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize) + 1;
            gint iy = 2*(i/jsize);
            gdouble *mrow = m + i*(i + 1)/2;

            for (j = 0; j <= i; j++) {
                gint jx = 2*(j % jsize) + 1;
                gint jy = 2*(j/jsize);

                mrow[j] = sumsx[ix*col_n + jx]*sumsy[iy*row_n + jy];
            }
        }
        /* Construct the subrhs */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize) + 1;
            gint iy = 2*(i/jsize);

            rhs[i] = coeffs[iy*col_n + ix];
        }
        /* Solve */
        if (!gwy_math_choleski_decompose(thissize, m)) {
            gwy_clear(coeffs, size);
            goto fail;
        }
        gwy_math_choleski_solve(thissize, m, rhs);
        /* Copy back */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize) + 1;
            gint iy = 2*(i/jsize);

            coeffs[iy*col_n + ix] = rhs[i];
        }
    }

    /* (Odd, Even) */
    isize = row_n/2;
    jsize = (col_n + 1)/2;
    thissize = jsize*isize;
    if (thissize) {
        /* Construct the submatrix */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize);
            gint iy = 2*(i/jsize) + 1;
            gdouble *mrow = m + i*(i + 1)/2;

            for (j = 0; j <= i; j++) {
                gint jx = 2*(j % jsize);
                gint jy = 2*(j/jsize) + 1;

                mrow[j] = sumsx[ix*col_n + jx]*sumsy[iy*row_n + jy];
            }
        }
        /* Construct the subrhs */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize);
            gint iy = 2*(i/jsize) + 1;

            rhs[i] = coeffs[iy*col_n + ix];
        }
        /* Solve */
        if (!gwy_math_choleski_decompose(thissize, m)) {
            gwy_clear(coeffs, size);
            goto fail;
        }
        gwy_math_choleski_solve(thissize, m, rhs);
        /* Copy back */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize);
            gint iy = 2*(i/jsize) + 1;

            coeffs[iy*col_n + ix] = rhs[i];
        }
    }

    /* (Odd, Odd) */
    isize = row_n/2;
    jsize = col_n/2;
    thissize = jsize*isize;
    if (thissize) {
        /* Construct the submatrix */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize) + 1;
            gint iy = 2*(i/jsize) + 1;
            gdouble *mrow = m + i*(i + 1)/2;

            for (j = 0; j <= i; j++) {
                gint jx = 2*(j % jsize) + 1;
                gint jy = 2*(j/jsize) + 1;

                mrow[j] = sumsx[ix*col_n + jx]*sumsy[iy*row_n + jy];
            }
        }
        /* Construct the subrhs */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize) + 1;
            gint iy = 2*(i/jsize) + 1;

            rhs[i] = coeffs[iy*col_n + ix];
        }
        /* Solve */
        if (!gwy_math_choleski_decompose(thissize, m)) {
            gwy_clear(coeffs, size);
            goto fail;
        }
        gwy_math_choleski_solve(thissize, m, rhs);
        /* Copy back */
        for (i = 0; i < thissize; i++) {
            gint ix = 2*(i % jsize) + 1;
            gint iy = 2*(i/jsize) + 1;

            coeffs[iy*col_n + ix] = rhs[i];
        }
    }

fail:
    g_free(m);
    g_free(rhs);
    g_free(sumsx);
    g_free(sumsy);

    return coeffs;
}

/**
 * gwy_field_fit_legendre:
 * @field: A data field.
 * @col_degree: Degree of polynomial to fit column-wise (x-coordinate).
 * @row_degree: Degree of polynomial to fit row-wise (y-coordinate).
 * @coeffs: (nullable):
 *          An array of size (@row_degree+1)*(@col_degree+1) to store the coefficients to, or %NULL (a fresh array is
 *          allocated then).
 *
 * Fits two-dimensional Legendre polynomial to a data field.
 *
 * See gwy_field_area_fit_legendre() for details.
 *
 * Returns: Either @coeffs if it was not %NULL, or a newly allocated array with coefficients.
 **/
gdouble*
gwy_field_fit_legendre(GwyField *field,
                       gint col_degree, gint row_degree,
                       gdouble *coeffs)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    return gwy_field_area_fit_legendre(field, 0, 0, field->xres, field->yres,
                                       col_degree, row_degree, coeffs);
}

/**
 * gwy_field_area_subtract_legendre:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @col_degree: Degree of polynomial to subtract column-wise (x-coordinate).
 * @row_degree: Degree of polynomial to subtract row-wise (y-coordinate).
 * @coeffs: An array of size (@row_degree+1)*(@col_degree+1) with coefficients, see gwy_field_area_fit_legendre()
 *          for details.
 *
 * Subtracts a two-dimensional Legendre polynomial fit from a rectangular part of a data field.
 *
 * Due to the transform of coordinates to [-1,1] x [-1,1], this method can be used on an area of dimensions different
 * than the area the coefficients were calculated for.
 **/
void
gwy_field_area_subtract_legendre(GwyField *field,
                                 gint col, gint row,
                                 gint width, gint height,
                                 gint col_degree, gint row_degree,
                                 const gdouble *coeffs)
{
    gint r, c, i, j, xres, col_n, row_n;
    gdouble *data, *pmx, *pmy;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;
    g_return_if_fail(coeffs);
    g_return_if_fail(row_degree >= 0 && col_degree >= 0);
    xres = field->xres;
    data = field->priv->data;
    col_n = col_degree + 1;
    row_n = row_degree + 1;

    pmx = g_new0(gdouble, col_n + row_n);
    pmy = pmx + col_n;

    for (r = 0; r < height; r++) {
        legendre_all(2*r/(height - 1.0) - 1.0, row_degree, pmy);
        for (c = 0; c < width; c++) {
            gdouble z = data[(row + r)*xres + (col + c)];

            legendre_all(2*c/(width - 1.0) - 1.0, col_degree, pmx);
            for (i = 0; i < row_n; i++) {
                for (j = 0; j < col_n; j++)
                    z -= coeffs[i*col_n + j]*pmx[j]*pmy[i];
            }

            data[(row + r)*xres + (col + c)] = z;
        }
    }

    g_free(pmx);

    gwy_field_invalidate(field);
}

/**
 * gwy_field_subtract_legendre:
 * @field: A data field.
 * @col_degree: Degree of polynomial to subtract column-wise (x-coordinate).
 * @row_degree: Degree of polynomial to subtract row-wise (y-coordinate).
 * @coeffs: An array of size (@row_degree+1)*(@col_degree+1) with coefficients, see gwy_field_area_fit_legendre()
 *          for details.
 *
 * Subtracts a two-dimensional Legendre polynomial fit from a data field.
 **/
void
gwy_field_subtract_legendre(GwyField *field,
                            gint col_degree, gint row_degree,
                            const gdouble *coeffs)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_subtract_legendre(field, 0, 0, field->xres, field->yres,
                                     col_degree, row_degree, coeffs);
}

/**
 * gwy_field_area_fit_poly_max:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @max_degree: Maximum total polynomial degree, that is the maximum of m+n in x^n y^m terms.
 * @coeffs: (nullable):
 *          An array of size (@max_degree+1)*(@max_degree+2)/2 to store the coefficients to, or %NULL (a fresh array
 *          is allocated then).
 *
 * Fits two-dimensional polynomial with limited total degree to a rectangular part of a data field.
 *
 * See gwy_field_area_fit_legendre() for description.  This function differs by limiting the total maximum
 * degree, while gwy_field_area_fit_legendre() limits the maximum degrees in horizontal and vertical directions
 * independently.
 *
 * Returns: Either @coeffs if it was not %NULL, or a newly allocated array with coefficients.
 **/
gdouble*
gwy_field_area_fit_poly_max(GwyField *field,
                            gint col, gint row,
                            gint width, gint height,
                            gint max_degree,
                            gdouble *coeffs)
{
    gint r, c, i, j, size, xres, degree_n;
    gint ix, jx, iy, jy;
    gdouble *data, *m, *pmx, *pmy, *sumsx, *sumsy;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return NULL;
    g_return_val_if_fail(max_degree >= 0, NULL);
    xres = field->xres;
    data = field->priv->data;
    degree_n = max_degree + 1;
    size = degree_n*(degree_n + 1)/2;
    g_return_val_if_fail(width*height > size, NULL);
    /* The maximum necessary matrix size (order), it is approximately four times smaller thanks to separation of even
     * and odd polynomials */
    if (!coeffs)
        coeffs = g_new0(gdouble, size);
    else
        gwy_clear(coeffs, size);

    sumsx = g_new0(gdouble, degree_n*degree_n);
    sumsy = g_new0(gdouble, degree_n*degree_n);
    m = g_new(gdouble, MAX(size*(size + 1)/2, 2*degree_n));
    /* pmx, pmy and m are not needed at the same time, reuse it */
    pmx = m;
    pmy = m + degree_n;

    /* Calculate <P_m(x) P_n(y) z(x,y)> (normalized to complete area) */
    for (r = 0; r < height; r++) {
        legendre_all(2*r/(height - 1.0) - 1.0, max_degree, pmy);
        for (c = 0; c < width; c++) {
            gdouble z = data[(row + r)*xres + (col + c)];

            legendre_all(2*c/(width - 1.0) - 1.0, max_degree, pmx);
            for (i = 0; i < degree_n; i++) {
                for (j = 0; j < degree_n - i; j++)
                    coeffs[i*(2*degree_n + 1 - i)/2 + j] += z*pmx[j]*pmy[i];
            }
        }
    }

    /* Calculate <P_m(x) P_a(x)> (normalized to single row). 3/4 of these values are zeroes, but it only takes
     * O(width) time. */
    for (c = 0; c < width; c++) {
        legendre_all(2*c/(width - 1.0) - 1.0, max_degree, pmx);
        for (i = 0; i < degree_n; i++) {
            for (j = 0; j < degree_n; j++)
                sumsx[i*degree_n + j] += pmx[i]*pmx[j];
        }
    }

    /* Calculate <P_n(y) P_b(y)> (normalized to single column) 3/4 of these values are zeroes, but it only takes
     * O(height) time. */
    for (r = 0; r < height; r++) {
        legendre_all(2*r/(height - 1.0) - 1.0, max_degree, pmy);
        for (i = 0; i < degree_n; i++) {
            for (j = 0; j < degree_n; j++)
                sumsy[i*degree_n + j] += pmy[i]*pmy[j];
        }
    }

    /* Construct the matrix */
    for (iy = 0; iy < degree_n; iy++) {
        for (jy = 0; jy < degree_n - iy; jy++) {
            gdouble *mrow;

            i = iy*(2*degree_n + 1 - iy)/2 + jy;
            mrow = m + i*(i + 1)/2;
            for (ix = 0; ix < degree_n; ix++) {
                for (jx = 0; jx < degree_n - ix; jx++) {
                    j = ix*(2*degree_n + 1 - ix)/2 + jx;
                    /* It is easier to go through all the coeffs and ignore the upper right triangle than to construct
                     * conditions directly for jy, jy, etc. */
                    if (j > i)
                        continue;
                    mrow[j] = sumsx[jy*degree_n + jx]*sumsy[iy*degree_n + ix];
                }
            }
        }
    }
    /* Solve */
    if (!gwy_math_choleski_decompose(size, m)) {
        gwy_clear(coeffs, size);
        goto fail;
    }
    gwy_math_choleski_solve(size, m, coeffs);

fail:
    g_free(m);
    g_free(sumsx);
    g_free(sumsy);

    return coeffs;
}

/**
 * gwy_field_fit_poly_max:
 * @field: A data field.
 * @max_degree: Maximum total polynomial degree, that is the maximum of m+n in x^n y^m terms.
 * @coeffs: (nullable):
 *          An array of size (@max_degree+1)*(@max_degree+2)/2 to store the coefficients to, or %NULL (a fresh array
 *          is allocated then).
 *
 * Fits two-dimensional polynomial with limited total degree to a data field.
 *
 * See gwy_field_area_fit_poly_max() for details.
 *
 * Returns: Either @coeffs if it was not %NULL, or a newly allocated array with coefficients.
 **/
gdouble*
gwy_field_fit_poly_max(GwyField *field,
                       gint max_degree,
                       gdouble *coeffs)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    return gwy_field_area_fit_poly_max(field, 0, 0, field->xres, field->yres, max_degree, coeffs);
}

/**
 * gwy_field_area_subtract_poly_max:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @max_degree: Maximum total polynomial degree, that is the maximum of m+n in x^n y^m terms.
 * @coeffs: An array of size (@row_degree+1)*(@col_degree+2)/2 with coefficients, see
 *          gwy_field_area_fit_poly_max() for details.
 *
 * Subtracts a two-dimensional polynomial with limited total degree from a rectangular part of a data field.
 *
 * Due to the transform of coordinates to [-1,1] x [-1,1], this method can be used on an area of dimensions different
 * than the area the coefficients were calculated for.
 **/
void
gwy_field_area_subtract_poly_max(GwyField *field,
                                 gint col, gint row,
                                 gint width, gint height,
                                 gint max_degree,
                                 const gdouble *coeffs)
{
    gint r, c, i, j, xres, degree_n;
    gdouble *data, *pmx, *pmy;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;
    g_return_if_fail(coeffs);
    g_return_if_fail(max_degree >= 0);
    xres = field->xres;
    data = field->priv->data;
    degree_n = max_degree + 1;

    pmx = g_new0(gdouble, 2*degree_n);
    pmy = pmx + degree_n;

    for (r = 0; r < height; r++) {
        legendre_all(2*r/(height - 1.0) - 1.0, max_degree, pmy);
        for (c = 0; c < width; c++) {
            gdouble z = data[(row + r)*xres + (col + c)];

            legendre_all(2*c/(width - 1.0) - 1.0, max_degree, pmx);
            for (i = 0; i < degree_n; i++) {
                for (j = 0; j < degree_n - i; j++)
                    z -= coeffs[i*(2*degree_n + 1 - i)/2 + j]*pmx[j]*pmy[i];
            }

            data[(row + r)*xres + (col + c)] = z;
        }
    }

    g_free(pmx);

    gwy_field_invalidate(field);
}

/**
 * gwy_field_subtract_poly_max:
 * @field: A data field.
 * @max_degree: Maximum total polynomial degree, that is the maximum of m+n in x^n y^m terms.
 * @coeffs: An array of size (@row_degree+1)*(@col_degree+2)/2 with coefficients, see
 *          gwy_field_area_fit_poly_max() for details.
 *
 * Subtracts a two-dimensional polynomial with limited total degree from a data field.
 **/
void
gwy_field_subtract_poly_max(GwyField *field,
                            gint max_degree,
                            const gdouble *coeffs)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_subtract_poly_max(field, 0, 0, field->xres, field->yres, max_degree, coeffs);
}

/**
 * gwy_field_area_fit_poly:
 * @field: A data field.
 * @mask: (nullable):
 *        Mask of values to take values into account, or %NULL for full @field.
 * @masking: Masking mode to use.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @nterms: The number of polynomial terms to take into account (half the number of items in @term_powers).
 * @term_powers: Array of size 2*@nterms describing the terms to fit.  Each terms is described by a couple of powers
 *               (powerx, powery).
 * @coeffs: (nullable): Array of size @nterms to store the coefficients to, or %NULL to allocate a new array.
 *
 * Fit a given set of polynomial terms to a rectangular part of a data field.
 *
 * The polynomial coefficients correspond to normalized coordinates that are always from the interval [-1,1] where -1
 * corresponds to the left/topmost pixel and 1 corresponds to the bottom/rightmost pixel of the area (not the entire
 * field).
 *
 * Returns: Either @coeffs if it was not %NULL, or a newly allocated array with coefficients.
 **/
gdouble*
gwy_field_area_fit_poly(GwyField *field,
                        GwyNield *mask,
                        GwyMaskingType masking,
                        gint col, gint row,
                        gint width, gint height,
                        gint nterms,
                        const gint *term_powers,
                        gdouble *coeffs)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return NULL;
    g_return_val_if_fail(nterms >= 0, NULL);

    if (!nterms)
        return coeffs;

    gint xres = field->xres;
    const gdouble *data = field->priv->data + row*xres + col;
    const gint *mdata = mask ? mask->priv->data + row*xres + col : NULL;

    if (!coeffs)
        coeffs = g_new0(gdouble, nterms);
    else
        gwy_clear(coeffs, nterms);

    gint np = nterms*(nterms + 1)/2;
    gdouble *m = g_new0(gdouble, np);
    gdouble *px = g_new(gdouble, nterms*width);

    /* Precalculate x powers which are the same in all rows. */
    for (gint j = 0; j < width; j++) {
        gdouble x = 2*j/(width - 1.0) - 1.0;

        for (gint k = 0; k < nterms; k++)
            px[j*nterms + k] = gwy_powi(x, term_powers[2*k]);
    }

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
        shared(data,mdata,m,coeffs,xres,np,width,height,row,col,nterms,masking,term_powers,px)
#endif
    {
        gint ifrom = gwy_omp_chunk_start(height);
        gint ito = gwy_omp_chunk_end(height);
        gdouble *tm = gwy_omp_if_threads_new0(m, np);
        gdouble *tcoeffs = gwy_omp_if_threads_new0(coeffs, nterms);
        gdouble p[nterms], py[nterms];

        for (gint i = ifrom; i < ito; i++) {
            gdouble y = 2*i/(height - 1.0) - 1.0;

            for (gint k = 0; k < nterms; k++)
                py[k] = gwy_powi(y, term_powers[2*k + 1]);

            for (gint j = 0; j < width; j++) {
                if (mdata && !nielded_included(mdata + i*xres + j, masking))
                    continue;

                for (gint k = 0; k < nterms; k++)
                    p[k] = px[j*nterms + k] * py[k];

                gdouble z = data[i*xres + j];
                gdouble *mm = tm;
                for (gint k = 0; k < nterms; k++) {
                    for (gint l = 0; l <= k; l++, mm++)
                        *mm += p[k]*p[l];
                    tcoeffs[k] += z*p[k];
                }
            }
        }

        gwy_omp_if_threads_sum_double(coeffs, tcoeffs, nterms);
        gwy_omp_if_threads_sum_double(m, tm, np);
    }

    if (!gwy_math_choleski_decompose(nterms, m))
        gwy_clear(coeffs, nterms);
    else
        gwy_math_choleski_solve(nterms, m, coeffs);

    g_free(m);
    g_free(px);

    return coeffs;
}

/**
 * gwy_field_fit_poly:
 * @field: A data field.
 * @mask: (nullable):
 *        Mask of values to take values into account, or %NULL for full @field.
 * @masking: Masking mode to use.
 * @nterms: The number of polynomial terms to take into account (half the number of items in @term_powers).
 * @term_powers: Array of size 2*@nterms describing the terms to fit.  Each terms is described by a couple of powers
 *               (powerx, powery).
 * @coeffs: (nullable): Array of size @nterms to store the coefficients to, or %NULL to allocate a new array.
 *
 * Fit a given set of polynomial terms to a data field.
 *
 * Returns: Either @coeffs if it was not %NULL, or a newly allocated array with coefficients.
 **/
gdouble*
gwy_field_fit_poly(GwyField *field,
                   GwyNield *mask,
                   GwyMaskingType masking,
                   gint nterms,
                   const gint *term_powers,
                   gdouble *coeffs)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    return gwy_field_area_fit_poly(field, mask, masking, 0, 0, field->xres, field->yres,
                                   nterms, term_powers, coeffs);
}

/**
 * gwy_field_area_subtract_poly:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @nterms: The number of polynomial terms to take into account (half the number of items in @term_powers).
 * @term_powers: Array of size 2*@nterms describing the fitted terms.  Each terms is described by a couple of powers
 *               (powerx, powery).
 * @coeffs: Array of size @nterms to store with the coefficients.
 *
 * Subtract a given set of polynomial terms from a rectangular part of a data
 * field.
 **/
void
gwy_field_area_subtract_poly(GwyField *field,
                             gint col, gint row,
                             gint width, gint height,
                             gint nterms,
                             const gint *term_powers,
                             const gdouble *coeffs)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;

    g_return_if_fail(nterms >= 0);
    g_return_if_fail(coeffs);

    if (!nterms)
        return;

    gint xres = field->xres;
    gdouble *data = field->priv->data + row*xres + col;

    /* Precalculate x powers which are the same in all rows. */
    gdouble *px = g_new(gdouble, nterms*width);
    for (gint j = 0; j < width; j++) {
        gdouble x = 2*j/(width - 1.0) - 1.0;

        for (gint k = 0; k < nterms; k++)
            px[j*nterms + k] = gwy_powi(x, term_powers[2*k]);
    }

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(data,coeffs,xres,width,height,row,col,nterms,term_powers,px)
#endif
    {
        gint ifrom = gwy_omp_chunk_start(height);
        gint ito = gwy_omp_chunk_end(height);
        gdouble py[nterms];

        for (gint i = ifrom; i < ito; i++) {
            gdouble y = 2*i/(height - 1.0) - 1.0;

            for (gint k = 0; k < nterms; k++)
                py[k] = gwy_powi(y, term_powers[2*k + 1]);

            for (gint j = 0; j < width; j++) {
                gdouble z = data[i*xres + j];

                for (gint k = 0; k < nterms; k++)
                    z -= coeffs[k] * px[j*nterms + k] * py[k];

                data[i*xres + j] = z;
            }
        }
    }
    g_free(px);

    gwy_field_invalidate(field);
}

/**
 * gwy_field_subtract_poly:
 * @field: A data field.
 * @nterms: The number of polynomial terms to take into account (half the number of items in @term_powers).
 * @term_powers: Array of size 2*@nterms describing the fitter terms.  Each terms is described by a couple of powers
 *               (powerx, powery).
 * @coeffs: Array of size @nterms to store with the coefficients.
 *
 * Subtract a given set of polynomial terms from a data field.
 **/
void
gwy_field_subtract_poly(GwyField *field,
                        gint nterms,
                        const gint *term_powers,
                        const gdouble *coeffs)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_subtract_poly(field, 0, 0, field->xres, field->yres, nterms, term_powers, coeffs);
}

/**
 * gwy_field_area_fit_local_planes:
 * @field: A data field.
 * @size: Neighbourhood size (must be at least 2).  It is centered around
 *        each pixel, unless @size is even when it sticks to the right.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @nresults: The number of requested quantities.
 * @types: The types of requested quantities.
 * @results: (nullable):
 *           An array to store quantities to, may be %NULL to allocate a new one which must be freed by caller then.
 *           If any item is %NULL, a new data field is allocated for it, existing data fields are resized to @width
 *           × @height.
 *
 * Fits a plane through neighbourhood of each sample in a rectangular part of a data field.
 *
 * The sample is always in the origin of its local (x,y) coordinate system, even if the neighbourhood is not centered
 * about it (e.g. because sample is on the edge of data field).  Z-coordinate is however not centered, that is
 * @GWY_PLANE_FIT_A is normal mean value.
 *
 * Returns: (transfer full):
 *          An array of data fields with requested quantities, that is @results unless it was %NULL and a new array
 *          was allocated.
 **/
GwyField**
gwy_field_area_fit_local_planes(GwyField *field,
                                gint size,
                                gint col, gint row,
                                gint width, gint height,
                                gint nresults,
                                const GwyPlaneFitQuantity *types,
                                GwyField **results)
{
    gdouble xreal, yreal, qx, qy, asymshfit;
    gint xres, yres, ri, i, j;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return NULL;
    g_return_val_if_fail(size > 1, NULL);
    if (!nresults)
        return NULL;
    g_return_val_if_fail(nresults > 0, NULL);
    g_return_val_if_fail(types, NULL);
    for (ri = 0; ri < nresults; ri++) {
        g_return_val_if_fail(types[ri] >= GWY_PLANE_FIT_A && types[ri] <= GWY_PLANE_FIT_S0_REDUCED, NULL);
        g_return_val_if_fail(!results || !results[ri] || GWY_IS_FIELD(results[ri]), NULL);
    }
    if (!results)
        results = g_new0(GwyField*, nresults);

    /* Allocate output data fields or fix their dimensions */
    xres = field->xres;
    yres = field->yres;
    qx = field->xreal/xres;
    qy = field->yreal/yres;
    xreal = qx*width;
    yreal = qy*height;
    for (ri = 0; ri < nresults; ri++) {
        if (!results[ri])
            results[ri] = gwy_field_new(width, height, xreal, yreal, FALSE);
        else {
            gwy_field_resize(results[ri], width, height);
            gwy_field_set_xreal(results[ri], xreal);
            gwy_field_set_yreal(results[ri], yreal);
        }
    }

    /* Fit local planes */
    asymshfit = (1 - size % 2)/2.0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i,j,ri) \
            shared(field,results,xres,yres,col,row,width,height,size,asymshfit,qx,qy,types,nresults)
#endif
    for (i = 0; i < height; i++) {
        gint ifrom = MAX(0, i + row - (size-1)/2);
        gint ito = MIN(yres-1, i + row + size/2);

        /* Prevent fitting plane through just one pixel on bottom edge when
         * size == 2 */
        if (G_UNLIKELY(ifrom == ito) && ifrom)
            ifrom--;

        for (j = 0; j < width; j++) {
            gint jfrom = MAX(0, j + col - (size-1)/2);
            gint jto = MIN(xres-1, j + col + size/2);
            gdouble *drect;
            gdouble sumz, sumzx, sumzy, sumzz, sumx, sumy, sumxx, sumxy, sumyy;
            gdouble n, bx, by, s0, s0r, det, shift;
            gdouble coeffs[GWY_PLANE_FIT_S0_REDUCED + 1];
            gint ii, jj;

            /* Prevent fitting plane through just one pixel on right edge when
             * size == 2 */
            if (G_UNLIKELY(jfrom == jto) && jfrom)
                jfrom--;

            drect = field->priv->data + ifrom*xres + jfrom;
            /* Compute sums with origin in top left corner */
            sumz = sumzx = sumzy = sumzz = 0.0;
            for (ii = 0; ii <= ito - ifrom; ii++) {
                gdouble *drow = drect + xres*ii;

                for (jj = 0; jj <= jto - jfrom; jj++) {
                    sumz += drow[jj];
                    sumzx += drow[jj]*jj;
                    sumzy += drow[jj]*ii;
                    sumzz += drow[jj]*drow[jj];
                }
            }
            n = (ito - ifrom + 1)*(jto - jfrom + 1);
            sumx = n*(jto - jfrom)/2.0;
            sumy = n*(ito - ifrom)/2.0;
            sumxx = sumx*(2*(jto - jfrom) + 1)/3.0;
            sumyy = sumy*(2*(ito - ifrom) + 1)/3.0;
            sumxy = sumx*sumy/n;

            /* Move origin to pixel, including in z coordinate, remembering average z value in shift */
            shift = ifrom - (i + row + asymshfit);
            sumxy += shift*sumx;
            sumyy += shift*(2*sumy + n*shift);
            sumzy += shift*sumz;
            sumy += n*shift;

            shift = jfrom - (j + col + asymshfit);
            sumxx += shift*(2*sumx + n*shift);
            sumxy += shift*sumy;
            sumzx += shift*sumz;
            sumx += n*shift;

            shift = -sumz/n;
            sumzx += shift*sumx;
            sumzy += shift*sumy;
            sumzz += shift*(2*sumz + n*shift);
            /* sumz = 0.0;  unused */

            /* Compute coefficients */
            det = sumxx*sumyy - sumxy*sumxy;
            bx = (sumzx*sumyy - sumxy*sumzy)/det;
            by = (sumzy*sumxx - sumxy*sumzx)/det;
            s0 = sumzz - bx*sumzx - by*sumzy;
            s0r = s0/(1.0 + bx*bx/qx/qx + by*by/qy/qy);

            coeffs[GWY_PLANE_FIT_A] = -shift;
            coeffs[GWY_PLANE_FIT_BX] = bx;
            coeffs[GWY_PLANE_FIT_BY] = by;
            coeffs[GWY_PLANE_FIT_ANGLE] = atan2(by, bx);
            coeffs[GWY_PLANE_FIT_SLOPE] = sqrt(bx*bx + by*by);
            coeffs[GWY_PLANE_FIT_S0] = s0;
            coeffs[GWY_PLANE_FIT_S0_REDUCED] = s0r;

            for (ri = 0; ri < nresults; ri++)
                results[ri]->priv->data[width*i + j] = coeffs[types[ri]];
        }
    }

    for (ri = 0; ri < nresults; ri++)
        gwy_field_invalidate(results[ri]);

    return results;
}

/**
 * gwy_field_fit_local_planes:
 * @field: A data field.
 * @size: Neighbourhood size.
 * @nresults: The number of requested quantities.
 * @types: The types of requested quantities.
 * @results: An array to store quantities to.
 *
 * Fits a plane through neighbourhood of each sample in a data field.
 *
 * See gwy_field_area_fit_local_planes() for details.
 *
 * Returns: (transfer full): An array of data fields with requested quantities.
 **/
GwyField**
gwy_field_fit_local_planes(GwyField *field,
                           gint size,
                           gint nresults,
                           const GwyPlaneFitQuantity *types,
                           GwyField **results)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    return gwy_field_area_fit_local_planes(field, size, 0, 0, field->xres, field->yres,
                                           nresults, types, results);
}

/**
 * gwy_field_area_local_plane_quantity:
 * @field: A data field.
 * @size: Neighbourhood size.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @type: The type of requested quantity.
 * @result: (nullable): A data field to store result to, or %NULL to allocate a new one.
 *
 * Convenience function to get just one quantity from gwy_field_area_fit_local_planes().
 *
 * Returns: (transfer full): @result if it is not %NULL, otherwise a newly allocated data field.
 **/
GwyField*
gwy_field_area_local_plane_quantity(GwyField *field,
                                    gint size,
                                    gint col, gint row,
                                    gint width, gint height,
                                    GwyPlaneFitQuantity type,
                                    GwyField *result)
{
    gwy_field_area_fit_local_planes(field, size, col, row, width, height, 1, &type, &result);
    return result;
}

/**
 * gwy_field_local_plane_quantity:
 * @field: A data field.
 * @size: Neighbourhood size.
 * @type: The type of requested quantity.
 * @result: (nullable): A data field to store result to, or %NULL to allocate a new one.
 *
 * Convenience function to get just one quantity from gwy_field_fit_local_planes().
 *
 * Returns: (transfer full): @result if it is not %NULL, otherwise a newly allocated data field.
 **/
GwyField*
gwy_field_local_plane_quantity(GwyField *field,
                               gint size,
                               GwyPlaneFitQuantity type,
                               GwyField *result)
{
    gwy_field_fit_local_planes(field, size, 1, &type, &result);

    return result;
}

/**
 * gwy_line_fit_line:
 * @line: A data line.
 * @a: (out): Height coefficient.
 * @b: (out): Slope coeficient.
 *
 * Finds line leveling coefficients.
 *
 * The coefficients can be used for line leveling using relation
 *
 * data[i] := data[i] - (av + bv*i)
 **/
void
gwy_line_fit_line(GwyLine *line, gdouble *a, gdouble *b)
{
    g_return_if_fail(GWY_IS_LINE(line));

    const gdouble *data = line->priv->data;
    gint res = line->res;
    if (res == 1) {
        if (a)
            *a = data[0];
        if (b)
            *b = 0.0;
        return;
    }

    /* These are already averages, not sums */
    gdouble xc = (res - 1.0)/2.0;
    gdouble sxx = (res*res - 1.0)/12.0;

    gdouble sxz = 0.0, sz = 0.0;
    for (gint i = 0; i < res; i++) {
        sz += data[i];
        sxz += data[i] * (i - xc);
    }
    gdouble bx = sxz/(res*sxx);

    if (a)
        *a = sz/res - bx*xc;
    if (b)
        *b = bx;
}

/**
 * gwy_line_line_level:
 * @line: A data line.
 * @a: Height coefficient.
 * @b: Slope coefficient.
 *
 * Levels a data line by subtraction.
 *
 * See gwy_line_fit_line() for deails.
 **/
void
gwy_line_line_level(GwyLine *line, gdouble a, gdouble b)
{
    g_return_if_fail(GWY_IS_LINE(line));

    gdouble *data = line->priv->data;
    gint res = line->res;
    for (gint i = 0; i < res; i++)
        data[i] -= a + b*i;
}

/**
 * gwy_line_rotate:
 * @line: A data line.
 * @angle: Angle of rotation (in radians), counterclockwise.
 * @interpolation: Interpolation method to use (can be only of two-point type).
 *
 * Levels a data line by rotation.
 *
 * This is operation similar to gwy_line_line_level(), but it does not change the angles between line segments
 * (on the other hand it introduces other deformations due to discretisation).
 **/
void
gwy_line_rotate(GwyLine *line,
                gdouble angle,
                GwyInterpolationType interpolation)
{
    g_return_if_fail(GWY_IS_LINE(line));
    gint res = line->res;
    if (angle == 0.0 || res < 2)
        return;

    gdouble *data = line->priv->data;
    gdouble step = line->real/res;

    /* INTERPOLATION: not checked, I'm not sure how this all relates to interpolation */
    gdouble *dx = g_new(gdouble, res);
    gdouble *dy = g_new(gdouble, res);

    dx[0] = 0;
    dy[0] = data[0];
    for (gint i = 1; i < res; i++) {
        gdouble as = atan2(data[i], i*step);
        gdouble radius = hypot(i*step, data[i]);
        dx[i] = radius*cos(as + angle);
        dy[i] = radius*sin(as + angle);
    }

    gint maxi = 0;
    for (gint i = 1; i < res; i++) {
        gdouble x = i*step;
        gint k = 0;
        do {
            k++;
        } while (k < res && dx[k] < x);

        if (k >= res-1) {
            maxi = i;
            break;
        }

        data[i] = gwy_interpolation_interpolate(x, dx[k-1], dy[k-1], dx[k], dy[k], interpolation);
    }
    g_free(dx);
    g_free(dy);

    if (maxi != 0) {
        line->real *= maxi/(double)res;
        line->res = maxi;
        line->priv->data = g_renew(gdouble, data, maxi);
    }

    if (line->res != res)
        gwy_line_resample(line, res, interpolation);
}

/**
 * gwy_line_part_fit_poly:
 * @line: A data line.
 * @n: Polynom degree.
 * @coeffs: (nullable): An array of size @n+1 to store the coefficients to, or %NULL (a fresh array is allocated then).
 * @pos: Index where to start.
 * @len: Length of extracted segment.
 *
 * Fits a polynomial through a part of a data line.
 *
 * Please see gwy_line_fit_poly() for more details.
 *
 * Returns: The coefficients of the polynomial (@coeffs when it was not %NULL, otherwise a newly allocated array).
 **/
gdouble*
gwy_line_part_fit_poly(GwyLine *line,
                       gint pos, gint len,
                       gint n, gdouble *coeffs)
{
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return coeffs;
    g_return_val_if_fail(n >= 0, coeffs);

    gdouble *sumx = g_new0(gdouble, 2*n+1);
    if (!coeffs)
        coeffs = g_new0(gdouble, n+1);
    else
        gwy_clear(coeffs, n+1);

    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++) {
        gdouble x = i;
        gdouble y = data[i];
        gdouble xp = 1.0;
        for (gint j = 0; j <= n; j++) {
            sumx[j] += xp;
            coeffs[j] += xp*y;
            xp *= x;
        }
        for (gint j = n+1; j <= 2*n; j++) {
            sumx[j] += xp;
            xp *= x;
        }
    }

    gdouble *m = g_new(gdouble, (n+1)*(n+2)/2);
    for (gint i = 0; i <= n; i++) {
        gdouble *row = m + i*(i+1)/2;

        for (gint j = 0; j <= i; j++)
            row[j] = sumx[i+j];
    }
    if (!gwy_math_choleski_decompose(n+1, m)) {
        g_warning("Line polynomial fit failed.");
        gwy_clear(coeffs, n+1);
    }
    else
        gwy_math_choleski_solve(n+1, m, coeffs);

    g_free(m);
    g_free(sumx);

    return coeffs;
}

/**
 * gwy_line_fit_poly:
 * @line: A data line.
 * @n: Polynom degree.
 * @coeffs: (nullable): An array of size @n+1 to store the coefficients to, or %NULL (a fresh array is allocated then).
 *
 * Fits a polynomial through a data line.
 *
 * Note @n is polynomial degree, so the size of @coeffs is @n+1.  X-values are indices in the data line.
 *
 * For polynomials of degree 0 and 1 it's better to use gwy_line_get_avg() and gwy_line_fit_line()
 * because they are faster.
 *
 * Returns: The coefficients of the polynomial (@coeffs when it was not %NULL, otherwise a newly allocated array).
 **/
gdouble*
gwy_line_fit_poly(GwyLine *line,
                  gint n, gdouble *coeffs)
{
    g_return_val_if_fail(GWY_IS_LINE(line), coeffs);
    return gwy_line_part_fit_poly(line, 0, line->res, n, coeffs);
}

/**
 * gwy_line_part_subtract_poly:
 * @line: A data line.
 * @n: Polynom degree.
 * @coeffs: An array of size @n+1 with polynomial coefficients to.
 * @pos: Index where to start.
 * @len: Length of extracted segment.
 *
 * Subtracts a polynomial from a part of a data line.
 **/
void
gwy_line_part_subtract_poly(GwyLine *line,
                            gint pos, gint len,
                            gint n, const gdouble *coeffs)
{
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return;
    g_return_if_fail(coeffs);
    g_return_if_fail(n >= 0);

    gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++) {
        gdouble val = 0.0;
        for (gint j = n; j; j--) {
            val += coeffs[j];
            val *= i;
        }
        val += coeffs[0];

        data[i] -= val;
    }
}

/**
 * gwy_line_subtract_poly:
 * @line: A data line.
 * @n: Polynom degree.
 * @coeffs: (array): An array of size @n+1 with polynomial coefficients to.
 *
 * Subtracts a polynomial from a data line.
 **/
void
gwy_line_subtract_poly(GwyLine *line,
                       gint n,
                       const gdouble *coeffs)
{
    g_return_if_fail(GWY_IS_LINE(line));
    gwy_line_part_subtract_poly(line, 0, line->res, n, coeffs);
}

/**
 * SECTION: level
 * @title: Levelling
 * @short_description: Leveling and background removal
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
