/*
 *  $Id: thumbnail.c 29932 2026-04-30 11:42:32Z yeti-dn $
 *  Copyright (C) 2025-2026 David Necas (Yeti)
 *  E-mail: yeti@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 <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <gtk/gtk.h>
#include <gdk/gdkcairo.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/dict.h"
#include "libgwyddion/arithmetic.h"
#include "libgwyddion/stats.h"
#include "libgwyui/gwyui.h"

#include "libgwyapp/gwyapp.h"
#include "libgwyapp/module-utils.h"
#include "libgwyapp/internal.h"

GwyField*
_gwy_app_create_brick_preview_field(GwyBrick *brick)
{
    GwyField *preview = gwy_brick_new_field_like_xy_plane(brick, FALSE);
    GwyNield *mask = gwy_field_new_nield_alike(preview);

    gwy_brick_mean_xy_plane(brick, preview);
    gwy_field_mark_outliers_iqr(preview, mask, 3.0, 3.0);
    gwy_field_laplace_solve(preview, mask, GWY_LAPLACE_MASKED, 1.0);
    g_object_unref(mask);

    return preview;
}

static gdouble
lawn_reduce_avg(gint ncurves, gint curvelength, const gdouble *curvedata, gpointer user_data)
{
    guint i, idx = GPOINTER_TO_UINT(user_data);
    gdouble s = 0.0;

    gwy_debug("nc %d, clen %d, data %p, idx %d", ncurves, curvelength, curvedata, idx);
    g_return_val_if_fail(idx < ncurves, 0.0);
    if (!curvelength)
        return s;

    curvedata += idx*curvelength;
    for (i = 0; i < curvelength; i++)
        s += curvedata[i];
    return s/curvelength;
}

GwyField*
_gwy_app_create_lawn_preview_field(GwyLawn *lawn)
{
    gint xres = gwy_lawn_get_xres(lawn);
    gint yres = gwy_lawn_get_yres(lawn);
    gdouble xreal = gwy_lawn_get_xreal(lawn);
    gdouble yreal = gwy_lawn_get_yreal(lawn);
    GwyField *preview = gwy_field_new(xres, yres, xreal, yreal, FALSE);
    GwyNield *mask = gwy_field_new_nield_alike(preview);

    gwy_lawn_reduce_to_plane(lawn, preview, lawn_reduce_avg, GUINT_TO_POINTER(0));
    /* FIXME: We should replace all pixels where there are no data (curve length is 0), as in change-preview-cmap.c.
     * Or do it consistently somehow… */
    gwy_field_mark_outliers_iqr(preview, mask, 3.0, 3.0);
    gwy_field_laplace_solve(preview, mask, GWY_LAPLACE_MASKED, 1.0);
    g_object_unref(mask);

    gwy_unit_assign(gwy_field_get_unit_z(preview), gwy_lawn_get_unit_curve(lawn, 0));

    return preview;
}

static GwyField*
make_thumbnail_field(GwyField *field,
                     gint *width,
                     gint *height)
{
    gint xres, yres;
    gdouble scale;

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    if (xres == *width && yres == *height)
        g_object_ref(field);
    else {
        scale = MAX(xres/(gdouble)*width, yres/(gdouble)*height);
        xres = xres/scale;
        yres = yres/scale;
        xres = CLAMP(xres, 2, *width);
        yres = CLAMP(yres, 2, *height);
        field = gwy_field_new_resampled(field, xres, yres, GWY_INTERPOLATION_NNA);
    }

    *width = xres;
    *height = yres;

    return field;
}

static GwyNield*
make_thumbnail_nield(GwyNield *nield,
                     gint *width,
                     gint *height)
{
    gint xres, yres;
    gdouble scale;

    xres = gwy_nield_get_xres(nield);
    yres = gwy_nield_get_yres(nield);
    if (xres == *width && yres == *height)
        g_object_ref(nield);
    else {
        scale = MAX(xres/(gdouble)*width, yres/(gdouble)*height);
        xres = xres/scale;
        yres = yres/scale;
        xres = CLAMP(xres, 2, *width);
        yres = CLAMP(yres, 2, *height);
        nield = gwy_nield_new_resampled(nield, xres, yres);
    }

    *width = xres;
    *height = yres;

    return nield;
}

static GdkPixbuf*
render_image_thumbnail(GwyField *field,
                       const gchar *gradname,
                       GwyColorMappingType mapping,
                       gint width,
                       gint height,
                       gdouble *pmin,
                       gdouble *pmax)
{
    GwyGradient *gradient = gwy_gradients_get_gradient(gradname);
    GwyField *render_field = make_thumbnail_field(field, &width, &height);
    GdkPixbuf *pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, width, height);
    gwy_draw_field_pixbuf(pixbuf, render_field, gradient, mapping, pmin, pmax);
    g_object_unref(render_field);

    return pixbuf;
}

static GdkPixbuf*
render_mask_thumbnail(GwyNield *mask,
                      const GwyRGBA *color,
                      gint width,
                      gint height)
{
    GwyNield *render_nield = make_thumbnail_nield(mask, &width, &height);
    GdkPixbuf *pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, width, height);
    gwy_draw_nield_pixbuf_mask(pixbuf, render_nield, color);
    g_object_unref(render_nield);

    return pixbuf;
}

/**
 * gwy_render_image_thumbnail:
 * @data: A data container.
 * @id: Data channel id.
 * @max_width: Maximum width of the created pixbuf, it must be at least 2.
 * @max_height: Maximum height of the created pixbuf, it must be at least 2.
 *
 * Creates a channel thumbnail.
 *
 * Returns: A newly created pixbuf with channel thumbnail.  It keeps the aspect ratio of the data field while not
 *          exceeding @max_width and @max_height.
 **/
GdkPixbuf*
gwy_render_image_thumbnail(GwyFile *data,
                           gint id,
                           gint max_width,
                           gint max_height)
{
    g_return_val_if_fail(GWY_IS_FILE(data), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    g_return_val_if_fail(max_width > 1 && max_height > 1, NULL);

    GwyField *field, *sfield = NULL;
    GwyNield *mask;
    GdkPixbuf *pixbuf;

    if (!(field = gwy_file_get_image(data, id)))
        return NULL;

    gwy_dict_gis_object(GWY_DICT(data), gwy_file_key_image_picture(id), &sfield);
    const gchar *gradient = gwy_file_get_palette(data, GWY_FILE_IMAGE, id);

    if (sfield)
        pixbuf = render_image_thumbnail(sfield, gradient, GWY_COLOR_MAPPING_FULL, max_width, max_height, NULL, NULL);
    else {
        GwyColorMappingType mapping = gwy_file_get_color_mapping(data, GWY_FILE_IMAGE, id);
        gdouble min, max;
        if (mapping == GWY_COLOR_MAPPING_FIXED) {
            /* render_image_thumbnail() requires non-NULL min and max for the fixed mode. */
            if (!gwy_file_get_fixed_range(data, GWY_FILE_IMAGE, id, &min, NULL))
                min = gwy_field_min(field);
            if (!gwy_file_get_fixed_range(data, GWY_FILE_IMAGE, id, NULL, &max))
                max = gwy_field_max(field);
        }
        /* Make thumbnails of images with defects nicer. */
        if (mapping == GWY_COLOR_MAPPING_FULL)
            mapping = GWY_COLOR_MAPPING_AUTO;

        pixbuf = render_image_thumbnail(field, gradient, mapping, max_width, max_height, &min, &max);
    }

    if ((mask = gwy_file_get_image_mask(data, id))) {
        GwyRGBA color;
        if (!gwy_dict_gis_boxed(GWY_DICT(data), GWY_TYPE_RGBA, gwy_file_key_image_mask_color(id), &color))
            gwy_app_settings_get_default_mask_color(&color);
        GdkPixbuf *maskpbuf = render_mask_thumbnail(mask, &color, max_width, max_height);
        gdk_pixbuf_composite(maskpbuf, pixbuf,
                             0, 0, gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf), 0, 0,
                             1.0, 1.0, GDK_INTERP_NEAREST, 255);
        g_object_unref(maskpbuf);
    }

    return pixbuf;
}

static gboolean
render_picture_thumbnail_common(GwyFile *file, GwyDataKind data_kind, gint id,
                                gint max_width, gint max_height,
                                GdkPixbuf **ppxibuf)
{
    *ppxibuf = NULL;
    g_return_val_if_fail(GWY_IS_FILE(file), FALSE);
    g_return_val_if_fail(id >= 0, FALSE);
    g_return_val_if_fail(max_width > 1 && max_height > 1, FALSE);

    GwyDict *dict = GWY_DICT(file);
    GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_NONE, .suffix = NULL };
    if (!gwy_dict_get_object(dict, gwy_form_file_key(&parsed)))
        return FALSE;

    GwyField *field = NULL;
    parsed.piece = GWY_FILE_PIECE_PICTURE;
    if (!gwy_dict_gis_object(dict, gwy_form_file_key(&parsed), &field)) {
        GdkPixbuf *pixbuf = *ppxibuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, max_width, max_height);
        gdk_pixbuf_fill(pixbuf, 0);
        return FALSE;
    }

    const gchar *gradient = gwy_file_get_palette(file, data_kind, id);
    *ppxibuf = render_image_thumbnail(field, gradient, GWY_COLOR_MAPPING_FULL, max_width, max_height, NULL, NULL);
    return TRUE;
}

/**
 * gwy_render_volume_thumbnail:
 * @data: A file data container.
 * @id: Volume data id.
 * @max_width: Maximum width of the created pixbuf, it must be at least 2.
 * @max_height: Maximum height of the created pixbuf, it must be at least 2.
 *
 * Creates a volume thumbnail.
 *
 * Returns: A newly created pixbuf with volume data thumbnail.  It keeps the aspect ratio of the brick preview while
 *          not exceeding @max_width and @max_height.
 **/
GdkPixbuf*
gwy_render_volume_thumbnail(GwyFile *data,
                            gint id,
                            gint max_width,
                            gint max_height)
{
    GdkPixbuf *pixbuf = NULL;
    render_picture_thumbnail_common(data, GWY_FILE_VOLUME, id, max_width, max_height, &pixbuf);
    return pixbuf;
}

/**
 * gwy_render_xyz_thumbnail:
 * @data: A file data container.
 * @id: XYZ scattered data id.
 * @max_width: Maximum width of the created pixbuf, it must be at least 2.
 * @max_height: Maximum height of the created pixbuf, it must be at least 2.
 *
 * Creates an XYZ data thumbnail.
 *
 * Returns: A newly created pixbuf with XYZ data thumbnail.  It keeps the aspect ratio of the brick preview while not
 *          exceeding @max_width and @max_height.
 **/
GdkPixbuf*
gwy_render_xyz_thumbnail(GwyFile *data,
                         gint id,
                         gint max_width,
                         gint max_height)
{
    GdkPixbuf *pixbuf = NULL;
    gboolean have_picture = render_picture_thumbnail_common(data, GWY_FILE_XYZ, id, max_width, max_height, &pixbuf);
    // Complete failure or complete success.
    if (!pixbuf || have_picture)
        return pixbuf;

    // There is no picture, but we seem to have data. Try to render a thumbnail ourselves.
    GwyScatter *scatter = gwy_file_get_xyz(data, id);
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);

    const gchar *gradient = gwy_file_get_palette(data, GWY_FILE_XYZ, id);
    GwyField *raster = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    guint preview_type = 0; // XYZPreviewType in change-preview-xyz.c
    gint ivalue = 0;
    gboolean outliers = FALSE;
    gwy_dict_gis_boolean_by_name(gwy_app_settings_get(), "/app/xyz-preview/outliers", &outliers);
    _gwy_preview_settings_from_file(data, GWY_FILE_XYZ, id, &preview_type, &ivalue, &outliers);
    if (preview_type || ivalue >= gwy_scatter_get_n_values(scatter))
        ivalue = -1;
    GwyPreviewScatterFlags flags = (outliers ? GWY_PREVIEW_SCATTER_OUTLIERS : 0);
    gwy_preview_scatter_to_field(scatter, ivalue, raster, max_width, max_height, flags);
    pixbuf = render_image_thumbnail(raster, gradient, GWY_COLOR_MAPPING_FULL,
                                    max_width, max_height, NULL, NULL);
    g_object_unref(raster);

    return pixbuf;
}

static GdkPixbuf*
render_graph_model(GwyGraphModel *gmodel, gint max_width, gint max_height)
{
    static GwyGraph *graph = NULL;
    static GwyGraphModel *dummy_model = NULL;

    /* Special values indicating cleanup. */
    if (!gmodel) {
        if (graph) {
            g_object_unref(graph);
            g_object_unref(dummy_model);
        }
        return NULL;
    }

    if (!gtk_main_level())
        return NULL;

    if (!graph) {
        graph = GWY_GRAPH(gwy_graph_new(gmodel));
        dummy_model = gwy_graph_model_new();
        gwy_graph_set_axis_visible(graph, GTK_POS_LEFT, FALSE);
        gwy_graph_set_axis_visible(graph, GTK_POS_BOTTOM, FALSE);
        g_object_ref_sink(graph);
        gtk_widget_show_all(GTK_WIDGET(graph));
    }
    else
        gwy_graph_set_model(GWY_GRAPH(graph), gmodel);

    gint width = MAX(160, max_width), height = MAX(120, max_height);
    GwyGraphArea *area = GWY_GRAPH_AREA(gwy_graph_get_area(graph));
    GdkRectangle allocation = { .x = 0, .y = 0, .width = width, .height = height };
    gtk_widget_size_allocate(GTK_WIDGET(area), &allocation);
    gtk_widget_show_all(GTK_WIDGET(area));

    gdouble min, max;
    gboolean is_logscale;
    gwy_graph_model_get_x_range(gmodel, &min, &max);
    gwy_graph_area_set_x_range(area, min, max);

    g_object_get(gmodel, "y-logarithmic", &is_logscale, NULL);
    gwy_graph_model_get_y_range(gmodel, &min, &max);
    if (is_logscale) {
        if (max > min) {
            gdouble d = max/min;
            d = pow(d, 0.07);
            min /= d;
            max *= d;
        }
        else if (max) {
            min = 0.5*max;
            max = 2.0*max;
        }
        else {
            min = 0.1;
            max = 10.0;
        }
    }
    else {
        if (max > min) {
            gdouble d = max - min;
            min -= 0.07*d;
            max += 0.07*d;
        }
        else if (max) {
            min = 0.5*max;
            max = 1.5*max;
        }
        else {
            min = -1.0;
            max = 1.0;
        }
    }
    gwy_graph_area_set_y_range(area, min, max);

    cairo_surface_t *surface = gdk_window_create_similar_image_surface(NULL, CAIRO_FORMAT_RGB24, width, height, 0);
    cairo_t *cr = cairo_create(surface);
    // This works:
    GTK_WIDGET_GET_CLASS(area)->draw(GTK_WIDGET(area), cr);
    // This does NOT work because it returns when the widget is now drawable, in particular it must be mapped.
    // TODO: Try again using offscreen windows, now that the coordinate transform in GraphArea is correct. However,
    // it is still a lot more work than just calling the draw method, which we wrote and know what it does.
    //gtk_widget_draw(GTK_WIDGET(area), cr);
    cairo_surface_flush(surface);
    GdkPixbuf *pixbuf, *big_pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, width, height);
    cairo_destroy(cr);
    cairo_surface_destroy(surface);

    if (width == max_width && height == max_height)
        pixbuf = big_pixbuf;
    else {
        pixbuf = gdk_pixbuf_scale_simple(big_pixbuf, max_width, max_height, GDK_INTERP_BILINEAR);
        g_object_unref(big_pixbuf);
    }

    /* Do not keep reference to the live graph model. */
    gwy_graph_set_model(graph, dummy_model);

    return pixbuf;
}

/**
 * gwy_render_graph_thumbnail:
 * @data: A file data container.
 * @id: Graph model data id.
 * @max_width: Maximum width of the created pixbuf, it must be at least 2.
 * @max_height: Maximum height of the created pixbuf, it must be at least 2.
 *
 * Creates a graph thumbnail.
 *
 * Note this function needs the GUI running (unlike the other thumbnail functions).  It cannot be used in a console
 * program.
 *
 * Returns: A newly created pixbuf with graph thumbnail.  Since graphs do not have natural width and height, its size
 *          will normally be exactly @max_width and @max_height.
 **/
/* TODO: Make it do something useful without GUI running. */
GdkPixbuf*
gwy_render_graph_thumbnail(GwyFile *data,
                           gint id,
                           gint max_width,
                           gint max_height)
{
    /* Special values indicating cleanup. */
    if (!data && id < 0 && !max_width && !max_height)
        return render_graph_model(NULL, 1, 1);

    g_return_val_if_fail(GWY_IS_FILE(data), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    g_return_val_if_fail(max_width > 1 && max_height > 1, NULL);

    GwyGraphModel *gmodel;
    if (!(gmodel = gwy_file_get_graph(data, id)))
        return NULL;

    return render_graph_model(gmodel, max_width, max_height);
}

/**
 * gwy_render_spectra_thumbnail:
 * @data: A file data container.
 * @id: Spectra data id.
 * @max_width: Maximum width of the created pixbuf, it must be at least 2.
 * @max_height: Maximum height of the created pixbuf, it must be at least 2.
 *
 * Creates a spectra thumbnail.
 *
 * Note this function needs the GUI running (unlike the other thumbnail functions).  It cannot be used in a console
 * program.
 *
 * Returns: A newly created pixbuf with spectra thumbnail.  Since graphs do not have natural width and height, its size
 *          will normally be exactly @max_width and @max_height.
 **/
/* TODO: Make it do something useful without GUI running. */
GdkPixbuf*
gwy_render_spectra_thumbnail(GwyFile *data,
                             gint id,
                             gint max_width,
                             gint max_height)
{
    /* Special values indicating cleanup. */
    if (!data && id < 0 && !max_width && !max_height)
        return render_graph_model(NULL, 1, 1);

    g_return_val_if_fail(GWY_IS_FILE(data), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    g_return_val_if_fail(max_width > 1 && max_height > 1, NULL);

    GwySpectra *spectra;
    if (!(spectra = gwy_file_get_spectra(data, id)))
        return NULL;

    // FIXME: This is wasteful: render_graph_model() already has its own private GraphModel and even keeps it around.
    GwyGraphModel *gmodel = gwy_graph_model_new();
    _gwy_app_fill_spectra_graph(spectra, gmodel);
    GdkPixbuf *pixbuf = render_graph_model(gmodel, max_width, max_height);
    g_object_unref(gmodel);

    return pixbuf;
}

/**
 * gwy_render_cmap_thumbnail:
 * @data: A file data container.
 * @id: Curve map data id.
 * @max_width: Maximum width of the created pixbuf, it must be at least 2.
 * @max_height: Maximum height of the created pixbuf, it must be at least 2.
 *
 * Creates a curve map thumbnail.
 *
 * Returns: A newly created pixbuf with curve map data thumbnail.  It keeps the aspect ratio of the lawn preview while
 *          not exceeding @max_width and @max_height.
 **/
GdkPixbuf*
gwy_render_cmap_thumbnail(GwyFile *data,
                          gint id,
                          gint max_width,
                          gint max_height)
{
    GdkPixbuf *pixbuf = NULL;
    render_picture_thumbnail_common(data, GWY_FILE_CMAP, id, max_width, max_height, &pixbuf);
    return pixbuf;
}

/**
 * SECTION: thumbnail
 * @title: Thumbnail
 * @short_description: Utility functions for creating data thumbnails.
 **/

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