/*
 *  $Id: pixbuf-render.c 29407 2026-01-30 15:34:09Z 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 "libgwyddion/macros.h"
#include "libgwyddion/threads.h"
#include "libgwyddion/omp.h"
#include "libgwyddion/stats.h"

#include "libgwyui/pixbuf-render.h"
#include "libgwyui/widget-impl-utils.h"

static gint* calc_cdh(GwyField *dfield, gint *cdh_size);

/**
 * gwy_draw_field_pixbuf:
 * @pixbuf: (transfer none):
 *          A Gdk pixbuf to draw to.
 * @field: (transfer none):
 *              A data field to draw.
 * @gradient: (transfer none):
 *            A colour gradient to draw with.
 * @mapping: False colour mapping type.
 * @minimum: (inout):
 *           The value corresponding to gradient start.
 * @maximum: (inout):
 *           The value corresponding to gradient end.
 *
 * Renders a data field to a pixbuf in specified mapping mode.
 *
 * The @minimum and @maximum arguments input values are used only in %GWY_COLOR_MAPPING_FIXED mode. They must not be
 * %NULL in such case.
 *
 * In all modes, the actual mapping start and end will be returned in @minimum and @maximum if they are not %NULL.
 * In %GWY_COLOR_MAPPING_FIXED mode the range is simply preserved.
 **/
void
gwy_draw_field_pixbuf(GdkPixbuf *pixbuf,
                      GwyField *field,
                      GwyGradient *gradient,
                      GwyColorMappingType mapping,
                      gdouble *minimum,
                      gdouble *maximum)
{
    if (mapping == GWY_COLOR_MAPPING_FULL) {
        gwy_draw_field_pixbuf_full(pixbuf, field, gradient);
        gwy_field_min_max(field, minimum, maximum);
    }
    else if (mapping == GWY_COLOR_MAPPING_ADAPT) {
        gwy_draw_field_pixbuf_adaptive(pixbuf, field, gradient);
        gwy_field_min_max(field, minimum, maximum);
    }
    else if (mapping == GWY_COLOR_MAPPING_AUTO) {
        gdouble amin, amax;
        gwy_field_autorange(field, &amin, &amax);
        gwy_draw_field_pixbuf_with_range(pixbuf, field, gradient, amin, amax);
        if (minimum)
            *minimum = amin;
        if (maximum)
            *maximum = amax;
    }
    else if (mapping == GWY_COLOR_MAPPING_FIXED) {
        g_return_if_fail(minimum);
        g_return_if_fail(maximum);
        gwy_draw_field_pixbuf_with_range(pixbuf, field, gradient, *minimum, *maximum);
    }
    else {
        g_assert_not_reached();
    }
}

static inline void
render_pixel_empty_range(gdouble x, gdouble minmax,
                         const GwyGradientPoint *points, gint npoints,
                         guchar *rgbpixel)
{
    gdouble r, g, b;
    gint i = -1;

    if (x < minmax)
        i = 0;
    else if (x > minmax)
        i = npoints-1;
    else if (npoints % 2)
        i = npoints/2;

    if (i == -1) {
        i = npoints/2;
        r = 0.5*(points[i-1].color.r + points[i].color.r);
        g = 0.5*(points[i-1].color.g + points[i].color.g);
        b = 0.5*(points[i-1].color.b + points[i].color.b);
    }
    else {
        r = points[i].color.r;
        g = points[i].color.g;
        b = points[i].color.b;
    }

    rgbpixel[0] = float_to_hex(r);
    rgbpixel[1] = float_to_hex(g);
    rgbpixel[2] = float_to_hex(b);
}

static inline gint
render_pixel(gdouble x,
             const GwyGradientPoint *points, gint npoints,
             gint i,
             guchar *rgbpixel)
{
    gdouble r, g, b;

    x = CLAMP(x, 0.0, 1.0);
    while (i < npoints && x > points[i].x)
        i++;
    while (i > 0 && x <= points[i].x)
        i--;

    i = CLAMP(i, 0, npoints-1);
    if (G_UNLIKELY(points[i].x == points[i+1].x))
        x = 0.5;
    else
        x = (x - points[i].x)/(points[i+1].x - points[i].x);

    r = (1.0 - x)*points[i].color.r + x*points[i+1].color.r;
    g = (1.0 - x)*points[i].color.g + x*points[i+1].color.g;
    b = (1.0 - x)*points[i].color.b + x*points[i+1].color.b;

    rgbpixel[0] = float_to_hex(r);
    rgbpixel[1] = float_to_hex(g);
    rgbpixel[2] = float_to_hex(b);

    return i;
}

/**
 * gwy_draw_field_pixbuf_with_range:
 * @pixbuf: (transfer none):
 *          A Gdk pixbuf to draw to.
 * @field: (transfer none):
 *              A data field to draw.
 * @gradient: (transfer none):
 *            A colour gradient to draw with.
 * @minimum: The value corresponding to gradient start.
 * @maximum: The value corresponding to gradient end.
 *
 * Renders a data field to a pixbuf with an explicit colour gradient range.
 *
 * @minimum and all smaller values are mapped to start of @gradient, @maximum and all greater values to its end,
 * values between are mapped linearly to @gradient.
 **/
void
gwy_draw_field_pixbuf_with_range(GdkPixbuf *pixbuf,
                                 GwyField *field,
                                 GwyGradient *gradient,
                                 gdouble minimum,
                                 gdouble maximum)
{
    int xres, yres, npoints, rowstride;
    const GwyGradientPoint *points;
    const gdouble *data;
    guchar *pixels;

    g_return_if_fail(GDK_IS_PIXBUF(pixbuf));
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_GRADIENT(gradient));

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    data = gwy_field_get_data_const(field);

    g_return_if_fail(xres == gdk_pixbuf_get_width(pixbuf));
    g_return_if_fail(yres == gdk_pixbuf_get_height(pixbuf));

    pixels = gdk_pixbuf_get_pixels(pixbuf);
    rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    points = gwy_gradient_get_points(gradient, &npoints);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(pixels,data,samples,xres,yres,rowstride,npoints,points,minimum,maximum)
#endif
    for (gint i = 0; i < yres; i++) {
        guchar *line = pixels + i*rowstride;
        const gdouble *row = data + i*xres;
        gint hint = 0;

        if (minimum == maximum) {
            for (gint j = 0; j < xres; j++)
                render_pixel_empty_range(row[j], minimum, points, npoints, line + 3*j);
        }
        else {
            for (gint j = 0; j < xres; j++)
                hint = render_pixel((row[j] - minimum)/(maximum - minimum), points, npoints, hint, line + 3*j);
        }
    }
}

/**
 * gwy_draw_field_pixbuf_full:
 * @pixbuf: (transfer none):
 *          A Gdk pixbuf to draw to.
 * @field: (transfer none):
 *              A data field to draw.
 * @gradient: (transfer none):
 *            A colour gradient to draw with.
 *
 * Renders a data field to a pixbuf with an auto-stretched colour gradient.
 *
 * Minimum data value is mapped to start of @gradient, maximum value to its end, values between are mapped linearly to
 * @gradient.
 **/
void
gwy_draw_field_pixbuf_full(GdkPixbuf *pixbuf,
                           GwyField *field,
                           GwyGradient *gradient)
{
    int xres, yres, npoints, rowstride;
    gdouble maximum, minimum;
    const GwyGradientPoint *points;
    const gdouble *data;
    guchar *pixels;

    g_return_if_fail(GDK_IS_PIXBUF(pixbuf));
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_GRADIENT(gradient));

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    data = gwy_field_get_data_const(field);

    g_return_if_fail(xres == gdk_pixbuf_get_width(pixbuf));
    g_return_if_fail(yres == gdk_pixbuf_get_height(pixbuf));

    gwy_field_min_max(field, &minimum, &maximum);
    pixels = gdk_pixbuf_get_pixels(pixbuf);
    rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    points = gwy_gradient_get_points(gradient, &npoints);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(pixels,data,samples,xres,yres,rowstride,points,npoints,minimum,maximum)
#endif
    for (gint i = 0; i < yres; i++) {
        guchar *line = pixels + i*rowstride;
        const gdouble *row = data + i*xres;
        gint hint = 0;

        if (minimum == maximum) {
            for (gint j = 0; j < xres; j++)
                render_pixel_empty_range(row[j], minimum, points, npoints, line + 3*j);
        }
        else {
            for (gint j = 0; j < xres; j++)
                hint = render_pixel((row[j] - minimum)/(maximum - minimum), points, npoints, hint, line + 3*j);
        }
    }
}

/**
 * gwy_draw_field_pixbuf_adaptive:
 * @pixbuf: (transfer none):
 *          A Gdk pixbuf to draw to.
 * @field: (transfer none):
 *              A data field to draw.
 * @gradient: (transfer none):
 *            A colour gradient to draw with.
 *
 * Renders a data field to a pixbuf with a colour gradient adaptively.
 *
 * The mapping from data field (minimum, maximum) range to gradient is nonlinear, deformed using inverse function to
 * height density cummulative distribution.
 **/
void
gwy_draw_field_pixbuf_adaptive(GdkPixbuf *pixbuf,
                               GwyField *field,
                               GwyGradient *gradient)
{
    gint xres, yres, rowstride, npoints, cdh_size;
    gdouble min, max, q, m;
    const GwyGradientPoint *points;
    const gdouble *data;
    guchar *pixels;
    gint *cdh;

    gwy_field_min_max(field, &min, &max);
    if (min == max) {
        gwy_draw_field_pixbuf_full(pixbuf, field, gradient);
        return;
    }

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    g_return_if_fail(xres == gdk_pixbuf_get_width(pixbuf));
    g_return_if_fail(yres == gdk_pixbuf_get_height(pixbuf));

    cdh = calc_cdh(field, &cdh_size);
    q = (cdh_size - 1.0)/(max - min);
    m = cdh[cdh_size-1];
    data = gwy_field_get_data_const(field);

    pixels = gdk_pixbuf_get_pixels(pixbuf);
    rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    points = gwy_gradient_get_points(gradient, &npoints);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i) \
            shared(pixels,data,samples,cdh,xres,yres,rowstride,q,min,m)
#endif
    for (gint i = 0; i < yres; i++) {
        guchar *line = pixels + i*rowstride;
        const gdouble *row = data + i*xres;
        gint hint = 0;

        for (gint j = 0; j < xres; j++) {
            gdouble v = (row[j] - min)*q;
            v = GWY_CLAMP(v, 0.0, m);
            gint h = (gint)v;
            v -= h;
            gdouble x = cdh[h]*(1.0 - v) + cdh[h+1]*v;
            hint = render_pixel(x/m, points, npoints, hint, line + 3*j);
        }
    }

    g_free(cdh);
}

/**
 * gwy_map_field_adaptive:
 * @field: (transfer none):
 *              A data field to draw.
 * @z: Array of @n data values to map.
 * @mapped: Output array of size @n where mapped @z values are to be stored.
 * @n: Number of elements in @z and @mapped.
 *
 * Maps ordinate values to interval [0,1] as gwy_draw_field_pixbuf_adaptive() would do.
 *
 * This is useful to find out which positions in the false colour gradient correspond to specific values.
 **/
void
gwy_map_field_adaptive(GwyField *field,
                       const gdouble *z,
                       gdouble *mapped,
                       guint n)
{
    gdouble min, max, cor, q, v, m;
    gint i, h, cdh_size;
    gint *cdh;

    gwy_field_min_max(field, &min, &max);
    if (min == max) {
        for (i = 0; i < n; i++)
            mapped[i] = 0.5;
        return;
    }

    cdh = calc_cdh(field, &cdh_size);
    q = (cdh_size - 1.0)/(max - min);
    cor = 1.0/cdh[cdh_size-1];

    m = cdh_size - 1.000001;
    for (i = 0; i < n; i++) {
        v = (z[i] - min)*q;
        v = GWY_CLAMP(v, 0.0, m);
        h = (gint)v;
        v -= h;
        v = (cdh[h]*(1.0 - v) + cdh[h+1]*v)*cor;
        mapped[i] = GWY_CLAMP(v, 0.0, 1.0);
    }

    g_free(cdh);
}

static gint*
calc_cdh(GwyField *dfield, gint *cdh_size)
{
    gdouble min, max;
    gint i, n, xres, yres;
    gint *cdh;

    xres = gwy_field_get_xres(dfield);
    yres = gwy_field_get_yres(dfield);
    gwy_field_min_max(dfield, &min, &max);

    n = *cdh_size = GWY_ROUND(pow(xres*yres, 2.0/3.0));
    cdh = g_new(guint, n);
    gwy_math_histogram(gwy_field_get_data_const(dfield), xres*yres,
                       min, max, n, cdh);
    for (i = 1; i < n; i++)
        cdh[i] += xres*yres/(2*n) + cdh[i-1];
    cdh[0] = 0;

    return cdh;
}

/* It may be useful to keep also a Field function since we might want to do things like render Nield to Field with
 * averaging (for example for thumbnails) and use that to render the mask. */
/**
 * gwy_draw_field_pixbuf_mask:
 * @pixbuf: (transfer none):
 *          A Gdk pixbuf to draw to.
 * @field: (transfer none):
 *         A data field to draw.
 * @color: A colour to use.
 *
 * Renders a data field to a pixbuf as a single-colour mask with varying opacity.
 *
 * Values equal or smaller to 0.0 are drawn as fully transparent, values greater or equal to 1.0 as fully opaque,
 * values between are linearly mapped to pixel opacity.
 **/
void
gwy_draw_field_pixbuf_mask(GdkPixbuf *pixbuf,
                           GwyField *field,
                           const GwyRGBA *color)
{
    g_return_if_fail(GDK_IS_PIXBUF(pixbuf));
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(color);

    guint32 pixel = gwy_rgba_to_pixbuf_pixel(color);
    guchar a = (pixel & 0xff);
    pixel |= 0xff;
    gdk_pixbuf_fill(pixbuf, pixel);
    if (!gdk_pixbuf_get_has_alpha(pixbuf))
        return;

    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    const gdouble *data = gwy_field_get_data_const(field);

    g_return_if_fail(xres == gdk_pixbuf_get_width(pixbuf));
    g_return_if_fail(yres == gdk_pixbuf_get_height(pixbuf));

    guchar *pixels = gdk_pixbuf_get_pixels(pixbuf);
    gint rowstride = gdk_pixbuf_get_rowstride(pixbuf);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(pixels,data,xres,yres,rowstride,a)
#endif
    for (gint i = 0; i < yres; i++) {
        guchar *pixline = pixels + i*rowstride + 3;
        const gdouble *drow = data + i*xres;

        for (gint j = xres; j; j--, drow++, pixline += 4)
            *pixline = (*drow >= 0.5) ? a : 0;
    }
}

/**
 * gwy_draw_nield_pixbuf_mask:
 * @pixbuf: (transfer none):
 *          A Gdk pixbuf to draw to.
 * @nield: (transfer none):
 *         A data nield to draw.
 * @color: A colour to use.
 *
 * Renders a data nield to a pixbuf as a single-colour mask with varying opacity.
 *
 * Values equal or smaller to 0.0 are drawn as fully transparent, values greater or equal to 1.0 as fully opaque,
 * values between are linearly mapped to pixel opacity.
 **/
void
gwy_draw_nield_pixbuf_mask(GdkPixbuf *pixbuf,
                           GwyNield *nield,
                           const GwyRGBA *color)
{
    g_return_if_fail(GDK_IS_PIXBUF(pixbuf));
    g_return_if_fail(GWY_IS_NIELD(nield));
    g_return_if_fail(color);

    guint32 pixel = gwy_rgba_to_pixbuf_pixel(color);
    guchar a = (pixel & 0xff);
    pixel |= 0xff;
    gdk_pixbuf_fill(pixbuf, pixel);
    if (!gdk_pixbuf_get_has_alpha(pixbuf))
        return;

    gint xres = gwy_nield_get_xres(nield), yres = gwy_nield_get_yres(nield);
    const gint *data = gwy_nield_get_data_const(nield);

    g_return_if_fail(xres == gdk_pixbuf_get_width(pixbuf));
    g_return_if_fail(yres == gdk_pixbuf_get_height(pixbuf));

    guchar *pixels = gdk_pixbuf_get_pixels(pixbuf);
    gint rowstride = gdk_pixbuf_get_rowstride(pixbuf);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(pixels,data,xres,yres,rowstride,a)
#endif
    for (gint i = 0; i < yres; i++) {
        guchar *pixline = pixels + i*rowstride + 3;
        const gint *drow = data + i*xres;

        for (gint j = xres; j; j--, drow++, pixline += 4)
            *pixline = a*(*drow > 0);
    }
}

/**
 * SECTION: pixbuf-render
 * @title: Pixbuf rendering
 * @short_description: Draw GwyFields to GdkPixbufs
 *
 * The simpliest method to render a #GwyField to a #GdkPixbuf with a false colour scale is
 * gwy_draw_field_pixbuf_full() which uniformly stretches the colour gradient from value minimum to maximum.
 * Functions gwy_draw_field_pixbuf_with_range() and gwy_draw_field_pixbuf_adaptive() offer other false
 * colour mapping possibilities.  A bit different is gwy_draw_field_pixbuf_mask() which represents the values as
 * opacities of a single colour.
 **/

/**
 * GwyColorMappingType:
 * @GWY_COLOR_MAPPING_FULL: Colour gradient is uniformly mapped to range from data minimum to maximum.
 * @GWY_COLOR_MAPPING_FIXED: Colour gradient is uniformly mapped to a fixed range, independent on data.
 * @GWY_COLOR_MAPPING_AUTO: Colour gradient is uniformly mapped to a range inside which most of data points lie, that
 *                          is height distribution tails are cut off.
 * @GWY_COLOR_MAPPING_ADAPT: Colour gradient is mapped nonuniformly, see gwy_draw_field_pixbuf_adaptive().
 *
 * Types of false colour gradient mapping in #GwyDataView.
 **/

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