/*
 *  $Id: linestats.c 29732 2026-04-03 20:31:34Z yeti-dn $
 *  Copyright (C) 2003-2025 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/math.h"
#include "libgwyddion/inttrans.h"
#include "libgwyddion/simplefft.h"
#include "libgwyddion/linestats.h"

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

/**
 * gwy_line_max:
 * @line: A data line.
 *
 * Finds the maximum value of a data line.
 *
 * Returns: The maximum value.
 **/
gdouble
gwy_line_max(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), -G_MAXDOUBLE);

    const gdouble *data = line->priv->data;
    gdouble max = data[0];
    gint res = line->res;
    for (gint i = 1; i < res; i++)
        max = fmax(max, data[i]);

    return max;
}

/**
 * gwy_line_min:
 * @line: A data line.
 *
 * Finds the minimum value of a data line.
 *
 * Returns: The minimum value.
 **/
gdouble
gwy_line_min(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), G_MAXDOUBLE);

    const gdouble *data = line->priv->data;
    gdouble min = data[0];
    gint res = line->res;
    for (gint i = 1; i < res; i++)
        min = fmin(min, data[i]);

    return min;
}

/**
 * gwy_line_min_pos_i:
 * @line: A data line.
 *
 * Finds the minimum pixel position of a data line.
 *
 * For historical reasons the value is returned as double, but it is always an integer.
 *
 * Returns: The minimum pixel position.
 **/
gint
gwy_line_min_pos_i(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0);

    gint res = line->res, pos = 0;
    const gdouble *data = line->priv->data;
    gdouble min = data[0];
    for (gint i = 1; i < res; i++) {
        if (data[i] < min) {
            min = data[i];
            pos = i;
        }
    }
    return pos;
}

/**
 * gwy_line_max_pos_i:
 * @line: A data line.
 *
 * Finds the maximum pixel position of a data line.
 *
 * For historical reasons the value is returned as double, but it is always an integer.
 *
 * Returns: The maximum pixel position.
 **/
gint
gwy_line_max_pos_i(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0);

    gint res = line->res, pos = 0;
    const gdouble *data = line->priv->data;
    gdouble max = data[0];
    for (gint i = 1; i < res; i++) {
        if (data[i] > max) {
            max = data[i];
            pos = i;
        }
    }
    return pos;
}

/**
 * gwy_line_min_pos_r:
 * @line: A data line.
 *
 * Finds the real minimum position in a data line.
 *
 * Returns: Real position for the minimum.
 **/
gdouble
gwy_line_min_pos_r(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_min_pos_i(line)*line->real/line->res + line->off;
}

/**
 * gwy_line_max_pos_r:
 * @line: A data line.
 *
 * Finds the real maximum position in a data line.
 *
 * Returns: Real position for the maximum.
 **/
gdouble
gwy_line_max_pos_r(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_max_pos_i(line)*line->real/line->res + line->off;
}

/**
 * gwy_line_mean:
 * @line: A data line.
 *
 * Computes average value of a data line.
 *
 * Returns: Average value
 **/
gdouble
gwy_line_mean(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_sum(line)/line->res;
}

/**
 * gwy_line_rms:
 * @line: A data line.
 *
 * Computes root mean square value of a data line.
 *
 * Returns: Root mean square deviation of values.
 **/
gdouble
gwy_line_rms(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);

    gint res = line->res;
    if (res < 2)
        return 0.0;

    gdouble avg = gwy_line_mean(line);
    const gdouble *data = line->priv->data;
    gdouble sum2 = 0.0;
    for (gint i = 0; i < res; i++)
        sum2 += (data[i] - avg)*(data[i] - avg);

    return sqrt(sum2/(res - 1));
}

/**
 * gwy_line_sum:
 * @line: A data line.
 *
 * Computes sum of all values in a data line.
 *
 * Returns: sum of all the values.
 **/
gdouble
gwy_line_sum(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);

    gint res = line->res;
    const gdouble *data = line->priv->data;
    gdouble sum = 0.0;
    for (gint i = 0; i < res; i++)
        sum += data[i];

    return sum;
}

/**
 * gwy_line_part_max:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Finds the maximum value of a part of a data line.
 *
 * Returns: Maximum within given interval.
 **/
gdouble
gwy_line_part_max(GwyLine *line,
                  gint pos, gint len)
{
    gdouble max = -G_MAXDOUBLE;
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return max;

    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++)
        max = fmax(max, data[i]);

    return max;
}

/**
 * gwy_line_part_min:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Finds the minimum value of a part of a data line.
 *
 * Returns: Minimum within given interval.
 **/
gdouble
gwy_line_part_min(GwyLine *line,
                  gint pos, gint len)
{
    gdouble min = G_MAXDOUBLE;
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return min;

    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++)
        min = fmin(min, data[i]);

    return min;
}

/**
 * gwy_line_part_min_max:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 * @min: (out): Location to store minimum to.
 * @max: (out): Location to store maximum to.
 *
 * Finds the minimum and maximum values of a part of a data line.
 **/
void
gwy_line_part_min_max(GwyLine *line,
                      gint pos, gint len,
                      gdouble *min, gdouble *max)
{
    /* If both are not requested do not bother finding them. */
    if (!max && min)
        *min = gwy_line_part_min(line, pos, len);
    if (max && !min)
        *max = gwy_line_part_max(line, pos, len);
    if (!max || !min)
        return;

    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return;

    gdouble min1 = G_MAXDOUBLE, max1 = -G_MAXDOUBLE;
    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++) {
        min1 = fmin(min1, data[i]);
        max1 = fmax(max1, data[i]);
    }

    *min = min1;
    *max = max1;
}

/**
 * gwy_line_min_max:
 * @line: A data line.
 * @min: (out): Location to store minimum to.
 * @max: (out): Location to store maximum to.
 *
 * Finds the minimum and maximum values of a data line.
 **/
void
gwy_line_min_max(GwyLine *line,
                 gdouble *min, gdouble *max)
{
    g_return_if_fail(GWY_IS_LINE(line));
    gwy_line_part_min_max(line, 0, line->res, min, max);
}

/**
 * gwy_line_part_mean:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes mean value of all values in a part of a data line.
 *
 * Returns: Average value within given interval.
 **/
gdouble
gwy_line_part_mean(GwyLine *a, gint pos, gint len)
{
    return len ? gwy_line_part_sum(a, pos, len)/len : 0.0;
}

/**
 * gwy_line_part_rms:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes root mean square value of a part of a data line.
 *
 * Returns: Root mean square deviation of heights within a given interval
 **/
gdouble
gwy_line_part_rms(GwyLine *line, gint pos, gint len)
{
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return 0.0;

    if (len < 2)
        return 0.0;
    gdouble avg = gwy_line_part_mean(line, pos, len);
    const gdouble *data = line->priv->data + pos;
    gdouble sum2 = 0.0;
    for (gint i = 0; i < len; i++)
        sum2 += (avg - data[i])*(avg - data[i]);

    return sqrt(sum2/(len - 1));
}

/**
 * gwy_line_ra:
 * @line: A data line.
 *
 * Computes the mean absolute deviation of a data line.
 *
 * Returns: The mean absolute deviation of height values.
 **/
gdouble
gwy_line_ra(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_part_ra(line, 0, line->res);
}

/**
 * gwy_line_skew:
 * @line: A data line.
 *
 * Computes the skew of a data line.
 *
 * Returns: The skew of height values.
 **/
gdouble
gwy_line_skew(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_part_skew(line, 0, line->res);
}

/**
 * gwy_line_kurtosis:
 * @line: A data line.
 *
 * Computes the kurtosis of a data line.
 *
 * Returns: The kurtosis of height values.
 *
 * Note the kurtosis returned by this function returns is the excess kurtosis which is zero for the Gaussian
 * distribution (not 3).
 **/
gdouble
gwy_line_kurtosis(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_part_kurtosis(line, 0, line->res);
}

/**
 * gwy_line_part_ra:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes mean absolute deviation value of a part of a data line.
 *
 * Returns: Mean absolute deviation of heights within a given interval.
 **/
gdouble
gwy_line_part_ra(GwyLine *line, gint pos, gint len)
{
    gdouble ra = 0.0;
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return ra;

    if (len < 2)
        return 0.0;

    gdouble avg = gwy_line_part_mean(line, pos, len);
    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++)
        ra += fabs(data[i] - avg);

    return ra/len * pow(len/(len - 1.0), 0.62);
}

/**
 * gwy_line_part_skew:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes skew value of a part of a data line.
 *
 * Returns: Skew of heights within a given interval.
 **/
gdouble
gwy_line_part_skew(GwyLine *line, gint pos, gint len)
{
    gdouble rms = 0.0, skew = 0.0;
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return skew;

    gdouble avg = gwy_line_part_mean(line, pos, len);
    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++) {
        gdouble d = data[i] - avg;
        rms += d*d;
        skew += d*d*d;
    }

    if (!rms)
        return 0.0;

    return skew*sqrt(len)/sqrt(rms*rms*rms);
}

/**
 * gwy_line_part_kurtosis:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes kurtosis value of a part of a data line.
 *
 * Note the kurtosis returned by this function returns is the excess kurtosis which is zero for the Gaussian
 * distribution (not 3).
 *
 * Returns: Kurtosis of heights within a given interval.
 **/
gdouble
gwy_line_part_kurtosis(GwyLine *line, gint pos, gint len)
{
    gdouble rms = 0.0, kurtosis = 0.0;
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return kurtosis;

    gdouble avg = gwy_line_part_mean(line, pos, len);
    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++) {
        gdouble d = data[i] - avg;
        d *= d;
        rms += d;
        kurtosis += d*d;
    }

    if (!rms)
        return 0.0;

    return kurtosis*len/(rms*rms) - 3.0;
}

/**
 * gwy_line_tan_beta0:
 * @line: A data line.
 *
 * Computes root mean square slope in a data line.
 *
 * Returns: Root mean square slope within a given interval.
 **/
gdouble
gwy_line_tan_beta0(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_part_tan_beta0(line, 0, line->res);
}

/**
 * gwy_line_part_tan_beta0:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes root mean square slope in a part of a data line.
 *
 * This is the root mean square of value derivatives, it is also proportional to the second derivative of both HHCF
 * and ACF at zero.
 *
 * This roughness quantity is also known as Dq.
 *
 * Returns: Root mean square slope within a given interval.
 **/
gdouble
gwy_line_part_tan_beta0(GwyLine *line, gint pos, gint len)
{
    gdouble tanbeta0 = 0.0;
    if (!_gwy_line_check_part(line, pos, len, FALSE) || len < 2)
        return tanbeta0;

    const gdouble *data = line->priv->data + pos;
    for (gint i = 1; i < len; i++)
        tanbeta0 += (data[i] - data[i-1])*(data[i] - data[i-1]);

    return sqrt(tanbeta0/(len - 1)) * line->res/line->real;
}

/**
 * gwy_line_part_variation:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes the total variation of a part of a data line.
 *
 * The total variation is estimated as the integral of the absolute value of local gradient.  For one dimensional
 * data, the variation reduces to the integral of absolute value of the derivative.  Its units are thus the same as
 * the value units of the line.  See also gwy_field_area_get_variation() for some more discussion.
 *
 * Returns: The total variation within a given interval.
 **/
gdouble
gwy_line_part_variation(GwyLine *line, gint pos, gint len)
{
    gdouble var = 0.0;
    if (!_gwy_line_check_part(line, pos, len, TRUE) || len < 2)
        return var;

    const gdouble *data = line->priv->data + pos;
    /* ∫ |y'| dx ≈ ∑ |Δy/Δx| Δx = ∑ |Δy|. So there is no other factor, except we correct for n-1. */
    for (gint i = 1; i < len; i++)
        var += fabs(data[i] - data[i-1]);

    return var * len/(len - 1.0);
}

/**
 * gwy_line_variation:
 * @line: A data line.
 *
 * Computes the total variation of a data line.
 *
 * See gwy_line_part_variation() for definition and discussion.
 *
 * Returns: The total variation.
 **/
gdouble
gwy_line_variation(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    return gwy_line_part_variation(line, 0, line->res);
}

/**
 * gwy_line_part_sum:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Computes sum of all values in a part of a data line.
 *
 * Returns: Sum of all values within the interval.
 **/
gdouble
gwy_line_part_sum(GwyLine *line, gint pos, gint len)
{
    gdouble sum = 0;
    if (!_gwy_line_check_part(line, pos, len, TRUE))
        return sum;

    const gdouble *data = line->priv->data + pos;
    for (gint i = 0; i < len; i++)
        sum += data[i];

    return sum;
}

static void
gwy_line_func_fft(GwyLine *line, GwyLine *target_line,
                  GwyFFTAreaFunc func)
{
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(GWY_IS_LINE(target_line));

    gdouble avg = gwy_line_mean(line);
    gint res = line->res;
    gint size = gwy_fft_find_nice_size(2*res);
    gwy_line_resize(target_line, res);
    gwy_line_clear(target_line);

    GwyLine *din = gwy_line_new(size, 1.0, FALSE);
    GwyLine *dout = gwy_line_new(size, 1.0, FALSE);
    gdouble *id = din->priv->data, *od = dout->priv->data, *data = line->priv->data;
    fftw_plan plan = gwy_fftw_plan_r2r_1d(size, id, od, FFTW_R2HC, FFTW_ESTIMATE);

    /* In principle, we only need this for ACF, but removing constant offset never hurts. */
    for (gint i = 0; i < res; i++)
        id[i] = data[i] - avg;

    func(plan, din, dout, target_line);
    g_object_unref(din);
    g_object_unref(dout);
    fftw_destroy_plan(plan);

    /* Set correct properties and units. */
    target_line->real = line->real;
    target_line->off = 0.0;
    gwy_line_multiply(target_line, 1.0/size);
    gwy_unit_assign(gwy_line_get_unit_x(target_line), gwy_line_get_unit_x(line));
    gwy_unit_power(gwy_line_get_unit_y(target_line), gwy_line_get_unit_y(line), 2);
}

/**
 * gwy_line_acf:
 * @line: A data line.
 * @target_line: Data line to store autocorrelation function to.  It will be
 *               resized to @line size.
 *
 * Coputes autocorrelation function and stores the values in @target_line
 **/
void
gwy_line_acf(GwyLine *line, GwyLine *target_line)
{
    gwy_line_func_fft(line, target_line, do_fft_acf);
}

/**
 * gwy_line_hhcf:
 * @line: A data line.
 * @target_line: Data line to store height-height function to.  It will be resized to @line size.
 *
 * Computes height-height correlation function and stores results in @target_line.
 **/
void
gwy_line_hhcf(GwyLine *line, GwyLine *target_line)
{
    gwy_line_func_fft(line, target_line, do_fft_hhcf);
}

/**
 * gwy_line_psdf:
 * @line: A data line.
 * @target_line: Data line to store power spectral density function to. It will be resized to @line size.
 * @windowing: Windowing method to use.
 *
 * Calculates the power spectral density function of a data line.
 **/
void
gwy_line_psdf(GwyLine *line,
              GwyLine *target_line,
              GwyWindowingType windowing)
{
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(GWY_IS_LINE(target_line));

    gint res = line->res;
    GwyLine *iin = gwy_line_new_alike(line, TRUE);
    GwyLine *rout = gwy_line_new_alike(line, FALSE);
    GwyLine *iout = gwy_line_new_alike(line, FALSE);
    gwy_line_resize(target_line, res/2);
    gwy_line_fft(line, iin, rout, iout, windowing, GWY_TRANSFORM_DIRECTION_FORWARD, TRUE, 2);

    gdouble *data = target_line->priv->data;
    gdouble *rdata = rout->priv->data;
    gdouble *idata = iout->priv->data;
    gdouble q = line->real/(res*2.0*G_PI);

    /* Calculate modulus */
    for (gint i = 0; i < res/2; i++)
        data[i] = q*(rdata[i]*rdata[i] + idata[i]*idata[i]);

    target_line->real = 2*G_PI*target_line->res/line->real;
    target_line->off = 0.0;

    g_object_unref(rout);
    g_object_unref(iin);
    g_object_unref(iout);

    /* Set proper units */
    GwyUnit *xunit = gwy_line_get_unit_x(line);
    GwyUnit *yunit = gwy_line_get_unit_y(line);
    GwyUnit *lineunit = gwy_line_get_unit_x(target_line);
    gwy_unit_power(lineunit, xunit, -1);

    lineunit = gwy_line_get_unit_y(target_line);
    gwy_unit_power_multiply(lineunit, yunit, 2, xunit, 1);
}

/**
 * gwy_line_distribution:
 * @line: A data line.
 * @distribution: Data line to put the distribution of @line values to. It will be resampled to @nstats samples
 *                (or the automatically chosen number of bins).
 * @ymin: Start of value range, pass @ymin = @ymax = 0.0 for the full range.
 * @ymax: End of value range.
 * @normalize_to_unity: %TRUE to normalize the integral to unity (including setting y-units of output to the inverse
 *                      of x-units), %FALSE to keep plain counts in the output (and set y-units to none).
 * @nstats: The requested number of histogram bins, pass a non-positive number to automatically choose a suitable
 *          number of bins.
 *
 * Calculates the distribution of data line values.
 *
 * This function is quite similar to gwy_line_dh(), the differences are: output normalization (chosen to make the
 * integral unity), output units (again set to make the integral unity), automated binning.
 *
 * Note the @i-th bin is [@i*@dx+@off,(@i+1)*@dx+@off] so the central value you probably want to use for plotting is
 * (@i+0.5)*@dx+@off (where @dx is the @distribution data line pixel size, @off is its offset).
 *
 * If all values are equal and @ymin, @ymax are not explictly specified, the range is chosen as [@v-|@v|/2,@v+|@v/2]
 * where @v is the unique value, except when @v=0, in which case the range is set to [-1,1].
 **/
void
gwy_line_distribution(GwyLine *line,
                      GwyLine *distribution,
                      gdouble ymin,
                      gdouble ymax,
                      gboolean normalize_to_unity,
                      gint nstats)
{
    GwyUnit *yunit;

    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(GWY_IS_LINE(distribution));

    /* Find reasonable binning */
    GWY_ORDER(gdouble, ymin, ymax);

    /* if ymin == ymax == 0 use the full range */
    if (!ymin && !ymax) {
        ymin = gwy_line_min(line);
        ymax = gwy_line_max(line);
        if (ymin > 0.0 && ymin <= 0.1*ymax)
            ymin = 0.0;
        else if (ymax < 0.0 && ymax >= 0.1*ymin)
            ymax = 0.0;
    }
    if (ymin == ymax) {
        if (ymax) {
            ymin -= 0.5*fabs(ymin);
            ymax += 0.5*fabs(ymax);
        }
        else {
            ymin = -1.0;
            ymax = 1.0;
        }
    }

    gint res = line->res;
    const gdouble *data = line->priv->data;
    if (nstats < 1) {
        gint ndata = 0;
        for (gint i = 0; i < res; i++) {
            if (data[i] >= ymin && data[i] <= ymax)
                ndata++;
        }
        nstats = floor(3.49*cbrt(ndata) + 0.5);
        nstats = MAX(nstats, 2);
    }

    gwy_debug("min: %g, max: %g, nstats: %d", ymin, ymax, nstats);
    gdouble s = (ymax - ymin)/(nstats - 1e-9);

    /* Fill histogram */
    gwy_line_resize(distribution, nstats);
    gwy_line_clear(distribution);

    guint *counts = g_new0(guint, nstats);
    gint ndata = gwy_math_histogram(data, res, ymin, ymax, nstats, counts);
    gdouble *ddata = distribution->priv->data;
    for (gint i = 0; i < nstats; i++)
        ddata[i] = counts[i];
    g_free(counts);

    /* Set proper units and scales */
    distribution->real = ymax - ymin;
    distribution->off = ymin;

    yunit = gwy_line_get_unit_y(line);
    gwy_unit_assign(gwy_line_get_unit_x(distribution), yunit);
    if (normalize_to_unity) {
        gwy_line_multiply(distribution, 1.0/(ndata*s));
        gwy_unit_power(gwy_line_get_unit_y(distribution), yunit, -1);
    }
    else
        gwy_unit_clear(gwy_line_get_unit_y(distribution));
}

/**
 * gwy_line_height_dist:
 * @line: A data line.
 * @target_line: Data line to store height distribution function to. It will be resized to @nsteps.
 * @ymin: Height distribution minimum value.
 * @ymax: Height distribution maximum value.
 * @nsteps: Number of histogram steps.
 *
 * Computes distribution of heights in interval [@ymin, @ymax).
 *
 * If the interval is (0, 0) it computes the distribution from real data minimum and maximum value.
 **/
void
gwy_line_height_dist(GwyLine *line,
                     GwyLine *target_line,
                     gdouble ymin, gdouble ymax,
                     gint nsteps)
{
    gint i, n;
    gdouble step;
    guint *counts;

    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(GWY_IS_LINE(target_line));

    n = line->res;
    gwy_line_resize(target_line, nsteps);
    gwy_line_clear(target_line);

    /* if ymin == ymax == 0 we want to set up histogram area */
    if (!ymin && !ymax) {
        ymin = gwy_line_min(line);
        ymax = gwy_line_max(line);
    }
    step = (ymax - ymin)/(nsteps - 1.0);

    counts = g_new0(guint, nsteps);
    n = gwy_math_histogram(line->priv->data, n, ymin, ymax, nsteps, counts);
    for (i = 0; i < nsteps; i++)
        target_line->priv->data[i] = counts[i];
    g_free(counts);

    gwy_line_multiply(target_line, 1.0/(n*step));
    target_line->off = ymin;
    target_line->real = ymax - ymin;
}

/**
 * gwy_line_angle_dist:
 * @line: A data line.
 * @target_line: Data line to store angle distribution function to.
 * @ymin: Angle distribution minimum value.
 * @ymax: Angle distribution maximum value.
 * @nsteps: Mumber of angular histogram steps.
 *
 * Computes distribution of angles in interval [@ymin, @ymax).
 *
 * If the interval is (0, 0) it computes the distribution from real data minimum and maximum angle value.
 **/
void
gwy_line_angle_dist(GwyLine *line,
                    GwyLine *target_line,
                    gdouble ymin, gdouble ymax,
                    gint nsteps)
{
    gint i, n, val;
    gdouble step, angle, imin;

    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(GWY_IS_LINE(target_line));

    n = line->res;
    gwy_line_resize(target_line, nsteps);
    gwy_line_clear(target_line);

    /* if ymin == ymax == 0 we want to set up histogram area */
    if (!ymin && !ymax) {
        ymin = G_MAXDOUBLE;
        ymax = -G_MAXDOUBLE;
        for (i = 0; i < n; i++) {
            angle = gwy_line_der(line, i);
            if (ymin > angle)
                ymin = angle;
            if (ymax < angle)
                ymax = angle;
        }
    }
    step = (ymax - ymin)/(nsteps - 1.0);
    imin = ymin/step;

    for (i = 0; i < n; i++) {
        val = (gint)(gwy_line_der(line, i)/step - imin);
        if (G_UNLIKELY(val < 0))
            val = 0; /* this should never happened */
        if (G_UNLIKELY(val >= nsteps))
            val = nsteps-1; /* this should never happened */
        target_line->priv->data[val] += 1.0;
    }
    target_line->real = ymax - ymin;
    target_line->off = ymin;
}

/**
 * gwy_line_length:
 * @line: A data line to compute length of.
 *
 * Calculates physical length of a data line.
 *
 * The length is calculated from approximation by straight segments between values.
 *
 * Returns: The line length.
 **/
gdouble
gwy_line_length(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    gint res = line->res;
    gdouble dx = line->real/res;
    if (G_UNLIKELY(res == 1))
        return dx;

    gdouble sum = 0.0;
    const gdouble *data = line->priv->data;
    for (gint i = 1; i < res; i++) {
        gdouble dy = data[i] - data[i-1];
        sum += sqrt(dx*dx + dy*dy);
    }

    /* We calculate length of inner part of a segment.  If we assume the average properties of border are the same as
     * of the inner part, we can simply multiply the sum with the total/inner length ratio */
    sum *= res/(res - 1.0);

    return sum;
}

/**
 * gwy_line_part_modus:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 * @histogram_steps: Number of histogram steps used for modus searching, pass a nonpositive number to autosize.
 *
 * Finds approximate modus of a data line part.
 *
 * As each number in the data line is usually unique, this function does not return modus of the data itself, but
 * modus of a histogram.
 *
 * Returns: The modus.
 **/
gdouble
gwy_line_part_modus(GwyLine *line,
                    gint pos, gint len,
                    gint histogram_steps)
{
    if (!_gwy_line_check_part(line, pos, len, TRUE))
        return 0.0;

    const gdouble *data = line->priv->data + pos;
    if (len == 1)
        return data[0];

    if (histogram_steps < 1) {
        histogram_steps = floor(3.49*cbrt(len) + 0.5);
        gwy_debug("histogram_steps = %d", histogram_steps);
    }

    gdouble min, max;
    gwy_line_part_min_max(line, pos, len, &min, &max);
    if (min >= max)
        return min;

    gint *histogram = g_new0(gint, histogram_steps);
    gwy_math_histogram(data, len, min, max, histogram_steps, histogram);

    gint m = 0;
    for (gint i = 1; i < histogram_steps; i++) {
        if (histogram[i] > histogram[m])
            m = i;
    }
    g_free(histogram);

    /* Average all values which contributed to the selected bin. */
    gint n = 0;
    gint sum = 0.0;
    for (gint i = 0; i < n; i++) {
        gint j = (data[i] - min)/(max - min)*histogram_steps;
        j = CLAMP(j, 0, histogram_steps-1);
        if (j == m) {
            sum += data[i];
            n++;
        }
    }

    gwy_debug("modus = %g", sum/n);

    return sum/n;
}

/**
 * gwy_line_modus:
 * @line: A data line.
 * @histogram_steps: Number of histogram steps used for modus searching, pass a nonpositive number to autosize.
 *
 * Finds approximate modus of a data line.
 *
 * See gwy_line_part_modus() for details and caveats.
 *
 * Returns: The modus.
 **/
gdouble
gwy_line_modus(GwyLine *line,
               gint histogram_steps)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0);

    return gwy_line_part_modus(line, 0, line->res, histogram_steps);
}

/**
 * gwy_line_part_median:
 * @line: A data line.
 * @pos: Index where to start.
 * @len: Length of the segment.
 *
 * Finds median of a data line part.
 *
 * Returns: The median.
 **/
gdouble
gwy_line_part_median(GwyLine *line,
                     gint pos, gint len)
{
    if (!_gwy_line_check_part(line, pos, len, FALSE))
        return 0.0;

    gdouble *d = g_memdup(line->priv->data + pos, len*sizeof(gdouble));
    gdouble med = gwy_math_median(d, len);
    g_free(d);

    return med;
}

/**
 * gwy_line_median:
 * @line: A data line.
 *
 * Finds median of a data line.
 *
 * Returns: The median.
 **/
gdouble
gwy_line_median(GwyLine *line)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0);

    return gwy_line_part_median(line, 0, line->res);
}

static inline void
add_sorted(gdouble *vals, gint len, gint *n, gdouble value)
{
    gint i, j, nn = *n;

    for (i = 0; i < nn; i++) {
        if (value > vals[i]) {
            for (j = MIN(len-1, nn); j > i; j--)
                vals[j] = vals[j-1];
            vals[i] = value;
            *n = MIN(nn+1, len);
            return;
        }
    }
    if (!nn && len) {
        vals[0] = value;
        *n = 1;
    }
}

/**
 * gwy_line_kth_peaks:
 * @line: A data line.
 * @m: Number of sampling lengths the line is split into.
 * @rank: Rank of the peak to find.  One means the highest peak, three the third highest, etc.
 * @peaks: %TRUE for peaks, %FALSE for valleys.  If you pass %FALSE, swap the meanings of peaks and valleys in the
 *         description.  Valley depths are positive.
 * @average: Calculate the average of the first @rank peaks instead of the height of @rank-th peak.
 * @pthreshold: Peak height threshold.  Peaks must stick above this threshold.
 * @vthreshold: Valley depth threshold.  Valleys must fall below this threshold.  The depth is a positive value.
 * @peakvalues: Array of length at least @m where the peak heights in each sampling length should be stored.
 *
 * Calculate k-th largers peaks or valleys in a data line split into given number of sampling lengths.
 *
 * This is a general function that can be used as the base for various standard roughness quantities such as Rp, Rpm,
 * Rv, Rvm or R3z.  It is assumed the line is already levelled, the form removed, etc.
 *
 * See gwy_line_count_peaks() for the description what is considered a peak.
 *
 * For larger thresholds and/or short lines some sampling lengths may contain fewer than the requested number of
 * peaks. If there are any peaks at all, the smallest peak height (even though it is not @rank-th) is used. If there
 * are no peaks, a large negative value is stored in the corresponding @peakvalues item.
 *
 * Returns: The actual number of peaks found (i.e. number of positive values filled in @peakvalues). If it is smaller
 *          than @m, some sampling lengths did not contain enough peaks.
 **/
gint
gwy_line_kth_peaks(GwyLine *line,
                   gint m, gint rank,
                   gboolean peaks, gboolean average,
                   gdouble pthreshold, gdouble vthreshold,
                   gdouble *peakvalues)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0);
    g_return_val_if_fail(rank > 0, 0);
    g_return_val_if_fail(m > 0, 0);
    g_return_val_if_fail(pthreshold >= 0.0, 0);
    g_return_val_if_fail(vthreshold >= 0.0, 0);
    g_return_val_if_fail(peakvalues, 0);

    if (!peaks)
        GWY_SWAP(gdouble, pthreshold, vthreshold);

    gdouble *pvals = g_new(gdouble, rank);
    gint res = line->res;
    gint npeaks = 0;
    for (gint mm = 0; mm < m; mm++) {
        gint pos = mm*res/m;
        gint len = (mm + 1)*res/m - pos;
        const gdouble *data = line->priv->data + pos;
        /* Peak is a segment of the line that goes above the positive threshold, separated segments that go below the
         * negative threshold. Valley is the opposite.  Between them there can be things that are neither. */
        gboolean seen_valley = FALSE;
        gdouble currpeak = 0.0;
        gint n = 0;
        for (gint i = 0; i < len; i++) {
            gdouble d = peaks ? data[i] : -data[i];
            if (d > pthreshold) {
                /* Finish the previous peak, if any occured. */
                if (seen_valley) {
                    if (currpeak > 0.0)
                        add_sorted(pvals, rank, &n, currpeak);
                    seen_valley = FALSE;
                    currpeak = 0.0;
                }
                if (d > currpeak)
                    currpeak = d;
            }
            else if (d < -vthreshold)
                seen_valley = TRUE;
        }
        if (currpeak > 0.0)
            add_sorted(pvals, rank, &n, currpeak);

        /* XXX: There are several reasonable things we can do when we do not find enough peaks... */
        if (n) {
            if (average)
                peakvalues[mm] = gwy_math_mean(pvals, n);
            else
                peakvalues[mm] = pvals[n-1];
            npeaks++;
        }
        else
            peakvalues[mm] = -G_MAXDOUBLE;
    }

    g_free(pvals);

    return npeaks;
}

/**
 * gwy_line_count_peaks:
 * @line: A data line.
 * @peaks: %TRUE for peaks, %FALSE for valleys.  If you pass %FALSE, swap the meanings of peaks and valleys in the
 *         description.  Valley depths are positive.
 * @pthreshold: Peak height threshold.  Peaks must stick above this threshold.
 * @vthreshold: Valley depth threshold.  Valleys must fall below this threshold.
 *
 * Counts peaks or valleys defined by thresholds in a data line.
 *
 * Peak is defined as a part of the profile that extends above the peak threshold and is separarted by valleys that
 * extend below the valley threshold.  For non-zero thresholds there may be parts between that are neither peaks not
 * valleys because the local maxima in them are insignificant.
 *
 * In either case, values of @pthreshold and @vthreshold must be non-negative. Usually one passes the same value for
 * both.
 *
 * Returns: The number of peaks found.
 **/
gint
gwy_line_count_peaks(GwyLine *line, gboolean peaks,
                     gdouble pthreshold, gdouble vthreshold)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0);
    g_return_val_if_fail(pthreshold >= 0.0, 0);
    g_return_val_if_fail(vthreshold >= 0.0, 0);

    gint res = line->res;
    const gdouble *data = line->priv->data;
    gint peakcount = 0;
    gboolean seen_interruption = TRUE;

    if (peaks) {
        for (gint i = 0; i < res; i++) {
            gdouble d = data[i];
            if (seen_interruption && d > pthreshold) {
                peakcount++;
                seen_interruption = FALSE;
            }
            else if (d < -vthreshold)
                seen_interruption = TRUE;
        }
    }
    else {
        for (gint i = 0; i < res; i++) {
            gdouble d = data[i];
            if (seen_interruption && d < -vthreshold) {
                peakcount++;
                seen_interruption = FALSE;
            }
            else if (d > pthreshold)
                seen_interruption = TRUE;
        }
    }

    return peakcount;
}

/**
 * gwy_line_xpm:
 * @line: A data line.
 * @m: Number of sampling lengths.
 * @k: Number of peaks to consider.
 *
 * Calculates a peak roughness quantity for a data line.
 *
 * Depending on @m and @k, the function can calculate Average Maximum Profile Peak Height @Rpm or Maximum Profile Peak
 * Height @Rp, @Pp, @Wp.
 *
 * Returns: The peak roughness quantity defined by @m and @k.
 **/
gdouble
gwy_line_xpm(GwyLine *line, gint m, gint k)
{
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    g_return_val_if_fail(m >= 1, 0.0);
    g_return_val_if_fail(m >= 1, 0.0);

    gdouble *peaks = g_new(gdouble, m);
    gwy_line_kth_peaks(line, m, k, TRUE, FALSE, 0.0, 0.0, peaks);

    gdouble Xpm = 0.0;
    gint n = 0;
    for (gint i = 0; i < m; i++) {
        if (peaks[i] > 0.0) {
            Xpm += peaks[i];
            n++;
        }
    }
    g_free(peaks);

    return n ? Xpm/n : 0.0;
}

/**
 * gwy_line_xvm:
 * @line: A data line.
 * @m: Number of sampling lengths.
 * @k: Number of valleys to consider.
 *
 * Calculates a valley roughness quantity for a data line.
 *
 * Depending on @m and @k, the function can calculate Average Maximum Profile Valley Depth @Rvm or Maximum Profile
 * Peak Depth @Rv, @Pv, @Wv.
 *
 * Returns: The valley roughness quantity defined by @m and @k.
 **/
gdouble
gwy_line_xvm(GwyLine *line, gint m, gint k)
{
    g_return_val_if_fail(m >= 1, 0.0);
    g_return_val_if_fail(GWY_IS_LINE(line), 0.0);
    g_return_val_if_fail(m >= 1, 0.0);

    gdouble *peaks = g_new(gdouble, m);
    gwy_line_kth_peaks(line, m, k, FALSE, FALSE, 0.0, 0.0, peaks);

    gdouble Xpm = 0.0;
    gint n = 0;
    for (gint i = 0; i < m; i++) {
        if (peaks[i] > 0.0) {
            Xpm += peaks[i];
            n++;
        }
    }

    return n ? Xpm/n : 0.0;
}

/**
 * gwy_line_xtm:
 * @line: A data line.
 * @m: Number of sampling lengths.
 * @k: Number of peaks and valleys to consider.
 *
 * Calculates a total roughness quantity for a data line.
 *
 * The total quantity is just the sum of the corresponding quantities obtained by gwy_line_xpm() and
 * gwy_line_xvm().
 *
 * Returns: The total roughness quantity defined by @m and @k.
 **/
gdouble
gwy_line_xtm(GwyLine *line, gint m, gint k)
{
    return gwy_line_xpm(line, m, k) + gwy_line_xvm(line, m, k);
}

/**
 * SECTION: linestats
 * @title: Line statistics
 * @short_description: One-dimensional statistical functions
 **/

/**
 * GwyLineStatQuantity:
 * @GWY_LINE_STAT_MEAN: Mean value.
 * @GWY_LINE_STAT_MEDIAN: Median.
 * @GWY_LINE_STAT_MINIMUM: Minimum value.
 * @GWY_LINE_STAT_MAXIMUM: Maximum value.
 * @GWY_LINE_STAT_RMS: Root mean square of deviations from the mean value.
 * @GWY_LINE_STAT_LENGTH: Line length.
 * @GWY_LINE_STAT_SLOPE: Overall line slope.
 * @GWY_LINE_STAT_TAN_BETA0: Root mean square slope.
 * @GWY_LINE_STAT_RA: Arithmetic mean surface roughness.
 * @GWY_LINE_STAT_RZ: Maximum height of the roughness profile.
 * @GWY_LINE_STAT_RT: Total height of the roughness profile.
 * @GWY_LINE_STAT_SKEW: Line skew.
 * @GWY_LINE_STAT_KURTOSIS: Line excess kurtosis (which is 0 for a Gaussaian distrubution, not 3).
 * @GWY_LINE_STAT_RANGE: Difference between maximum and minimum value.
 * @GWY_LINE_STAT_VARIATION: Variation (integral of absolute value).
 * @GWY_LINE_STAT_MINPOS: Minimum position along the line.
 * @GWY_LINE_STAT_MAXPOS: Maximum position along the line.
 *
 * Line statistical quantities to be requested with gwy_field_area_get_line_stats().
 **/

/* 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 : */
