/*
 *  $Id: grain-value.c 30012 2026-06-05 13:47:00Z yeti-dn $
 *  Copyright (C) 2007-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 <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/expr.h"
#include "libgwyddion/grain-value.h"

#include "libgwyddion/internal.h"

typedef struct {
    GwyGrainValueGroup group;
    const gchar *symbol_markup;
    const gchar *symbol;
    gint power_xy;
    gint power_z;
    guint flags;
} GwyGrainValueData;

typedef struct {
    const gchar* name;
    GwyGrainQuantity quantity;
    GwyGrainValueData data;
} BuiltinGrainValueInfo;

struct _GwyGrainValuePrivate {
    GwyGrainValueData data;
    GwyGrainQuantity builtin;
    gchar *expression;
};

static gpointer       resource_copy          (gpointer item);
static GwyGrainValue* create_grain_value     (const gchar *name,
                                              const GwyGrainValueData *data,
                                              gboolean is_const);
static void           resource_setup_builtins(GwyInventory *inventory);
static void           resource_dump          (GwyResource *resource,
                                              GString *str);
static GwyResource*   resource_parse         (const gchar *text);

// FIXME: This table duplicates unit power information from grain-quantities.c. But there is no way to fill it
// while keeping it const. Does keeping it const matter much?
static const BuiltinGrainValueInfo grain_values[] = {
    {
        N_("Grain number"), GWY_GRAIN_ID,
        {
            GWY_GRAIN_GROUP_ID,
            "id", "",
            0, 0, 0,
        }
    },
    {
        N_("Center x position"), GWY_GRAIN_CENTER_X,
        {
            GWY_GRAIN_GROUP_POSITION,
            "<i>x</i><sub>c</sub>", "x_c",
            1, 0, 0,
        }
    },
    {
        N_("Center y position"), GWY_GRAIN_CENTER_Y,
        {
            GWY_GRAIN_GROUP_POSITION,
            "<i>y</i><sub>c</sub>", "y_c",
            1, 0, 0,
        }
    },
    {
        N_("Minimum value"), GWY_GRAIN_MINIMUM,
        {
            GWY_GRAIN_GROUP_VALUE,
            "<i>z</i><sub>min</sub>", "z_min",
            0, 1, 0,
        }
    },
    {
        N_("Maximum value"), GWY_GRAIN_MAXIMUM,
        {
            GWY_GRAIN_GROUP_VALUE,
            "<i>z</i><sub>max</sub>", "z_max",
            0, 1, 0,
        }
    },
    {
        N_("Mean value"), GWY_GRAIN_MEAN,
        {
            GWY_GRAIN_GROUP_VALUE,
            "<i>z</i><sub>m</sub>", "z_m",
            0, 1, 0,
        }
    },
    {
        N_("Median value"), GWY_GRAIN_MEDIAN,
        {
            GWY_GRAIN_GROUP_VALUE,
            "<i>z</i><sub>med</sub>", "z_med",
            0, 1, 0,
        }
    },
    {
        N_("RMS value"), GWY_GRAIN_RMS,
        {
            GWY_GRAIN_GROUP_VALUE,
            "<i>z</i><sub>rms</sub>", "z_rms",
            0, 1, 0,
        }
    },
    {
        N_("Minimum value on boundary"), GWY_GRAIN_BOUNDARY_MINIMUM,
        {
            GWY_GRAIN_GROUP_VALUE,
            "<i>b</i><sub>min</sub>", "b_min",
            0, 1, 0,
        }
    },
    {
        N_("Maximum value on boundary"), GWY_GRAIN_BOUNDARY_MAXIMUM,
        {
            GWY_GRAIN_GROUP_VALUE,
            "<i>b</i><sub>max</sub>", "b_max",
            0, 1, 0,
        }
    },
    {
        N_("Pixel area"), GWY_GRAIN_PIXEL_AREA,
        {
            GWY_GRAIN_GROUP_AREA,
            "<i>A</i><sub>px</sub>", "A_px",
            0, 0, 0,
        }
    },
    {
        N_("Projected area"), GWY_GRAIN_PROJECTED_AREA,
        {
            GWY_GRAIN_GROUP_AREA,
            "<i>A</i><sub>0</sub>", "A_0",
            2, 0, 0,
        }
    },
    {
        N_("Surface area"), GWY_GRAIN_SURFACE_AREA,
        {
            GWY_GRAIN_GROUP_AREA,
            "<i>A</i><sub>s</sub>", "A_s",
            2, 0, GWY_GRAIN_SAME_UNITS,
        }
    },
    {
        N_("Equivalent disc radius"), GWY_GRAIN_EQUIV_DISC_RADIUS,
        {
            GWY_GRAIN_GROUP_AREA,
            "<i>r</i><sub>eq</sub>", "r_eq",
            1, 0, 0,
        }
    },
    {
        N_("Area above half-height"), GWY_GRAIN_HALF_HEIGHT_AREA,
        {
            GWY_GRAIN_GROUP_AREA,
            "<i>A</i><sub>h</sub>", "A_h",
            2, 0, 0,
        }
    },
    {
        N_("Area of convex hull"), GWY_GRAIN_CONVEX_HULL_AREA,
        {
            GWY_GRAIN_GROUP_AREA,
            "<i>A</i><sub>c</sub>", "A_c",
            2, 0, 0,
        }
    },
    {
        /* TRANSLATORS: Grain volume calculated using basis (foot) set to zero. */
        N_("Zero basis volume"), GWY_GRAIN_VOLUME_0,
        {
            GWY_GRAIN_GROUP_VOLUME,
            "<i>V</i><sub>0</sub>", "V_0",
            2, 1, 0,
        }
    },
    {
        /* TRANSLATORS: Grain volume calculated using basis (foot) set to the grain minimum. */
        N_("Grain minimum basis volume"), GWY_GRAIN_VOLUME_MIN,
        {
            GWY_GRAIN_GROUP_VOLUME,
            "<i>V</i><sub>min</sub>", "V_min",
            2, 1, 0,
        }
    },
    {
        /* TRANSLATORS: Grain volume calculated using basis (foot) set to the Laplacian background. */
        N_("Laplacian background basis volume"), GWY_GRAIN_VOLUME_LAPLACE,
        {
            GWY_GRAIN_GROUP_VOLUME,
            "<i>V</i><sub>L</sub>", "V_L",
            2, 1, 0,
        }
    },
    {
        N_("Perimeter"), GWY_GRAIN_PERIMETER,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>P</i>", "P",
            1, 0, 0,
        }
    },
    {
        N_("Minimum projection size"), GWY_GRAIN_MINIMUM_PROJECT_SIZE,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>D</i><sub>min</sub>", "D_min",
            1, 0, 0,
        }
    },
    {
        N_("Minimum projection direction"), GWY_GRAIN_MINIMUM_PROJECT_ANGLE,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>φ</i><sub>min</sub>", "phi_min",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Maximum projection size"), GWY_GRAIN_MAXIMUM_PROJECT_SIZE,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>D</i><sub>max</sub>", "D_max",
            1, 0, 0,
        }
    },
    {
        N_("Maximum projection direction"), GWY_GRAIN_MAXIMUM_PROJECT_ANGLE,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>φ</i><sub>max</sub>", "phi_max",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Feret length"), GWY_GRAIN_FERET_LENGTH,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>L</i><sub>F</sub>", "L_F",
            1, 0, 0,
        }
    },
    {
        N_("Maximum inscribed disc radius"), GWY_GRAIN_INSCRIBED_DISC_R,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>R</i><sub>i</sub>", "R_i",
            1, 0, 0,
        }
    },
    {
        N_("Maximum inscribed disc center x position"), GWY_GRAIN_INSCRIBED_DISC_X,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>x</i><sub>i</sub>", "x_i",
            1, 0, 0,
        }
    },
    {
        N_("Maximum inscribed disc center y position"), GWY_GRAIN_INSCRIBED_DISC_Y,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>y</i><sub>i</sub>", "y_i",
            1, 0, 0,
        }
    },
    {
        N_("Minimum circumcircle radius"), GWY_GRAIN_CIRCUMCIRCLE_R,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>R</i><sub>e</sub>", "R_e",
            1, 0, 0,
        }
    },
    {
        N_("Minimum circumcircle center x position"), GWY_GRAIN_CIRCUMCIRCLE_X,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>x</i><sub>e</sub>", "x_e",
            1, 0, 0,
        }
    },
    {
        N_("Minimum circumcircle center y position"), GWY_GRAIN_CIRCUMCIRCLE_Y,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>y</i><sub>e</sub>", "y_e",
            1, 0, 0,
        }
    },
    {
        N_("Mean radius"), GWY_GRAIN_MEAN_RADIUS,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>R</i><sub>m</sub>", "R_m",
            1, 0, 0,
        }
    },
    {
        N_("Mean edge distance"), GWY_GRAIN_MEAN_BOUNDARY_DISTANCE,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>d</i><sub>e</sub>", "d_e",
            1, 0, 0,
        }
    },
    {
        N_("Shape number"), GWY_GRAIN_SHAPE_NUMBER,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>F</i><sub>s</sub>", "F_s",
            0, 0, 0,
        }
    },
    {
        N_("Minimum Martin diameter"), GWY_GRAIN_MINIMUM_MARTIN_DIAMETER,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>M</i><sub>min</sub>", "M_min",
            1, 0, 0,
        }
    },
    {
        N_("Direction of minimum Martin diameter"), GWY_GRAIN_MINIMUM_MARTIN_ANGLE,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>ω</i><sub>min</sub>", "omega_min",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Maximum Martin diameter"), GWY_GRAIN_MAXIMUM_MARTIN_DIAMETER,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>M</i><sub>max</sub>", "M_max",
            1, 0, 0,
        }
    },
    {
        N_("Direction of maximum Martin diameter"), GWY_GRAIN_MAXIMUM_MARTIN_ANGLE,
        {
            GWY_GRAIN_GROUP_BOUNDARY,
            "<i>ω</i><sub>max</sub>", "omega_max",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Inclination θ"), GWY_GRAIN_SLOPE_THETA,
        {
            GWY_GRAIN_GROUP_SLOPE,
            "<i>θ</i>", "theta",
            0, 0, GWY_GRAIN_SAME_UNITS | GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Inclination φ"), GWY_GRAIN_SLOPE_PHI,
        {
            GWY_GRAIN_GROUP_SLOPE,
            "<i>φ</i>", "phi",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Curvature center x position"), GWY_GRAIN_CURVATURE_CENTER_X,
        {
            GWY_GRAIN_GROUP_CURVATURE,
            "<i>x</i><sub>0</sub>", "x_0",
            1, 0, 0,
        }
    },
    {
        N_("Curvature center y position"), GWY_GRAIN_CURVATURE_CENTER_Y,
        {
            GWY_GRAIN_GROUP_CURVATURE,
            "<i>y</i><sub>0</sub>", "y_0",
            1, 0, 0,
        }
    },
    {
        N_("Curvature center z value"), GWY_GRAIN_CURVATURE_CENTER_Z,
        {
            GWY_GRAIN_GROUP_CURVATURE,
            "<i>z</i><sub>0</sub>", "z_0",
            0, 1, 0,
        }
    },
    {
        N_("Curvature 1"), GWY_GRAIN_CURVATURE1,
        {
            GWY_GRAIN_GROUP_CURVATURE,
            "<i>κ</i><sub>1</sub>", "kappa_1",
            -1, 0, GWY_GRAIN_SAME_UNITS,
        }
    },
    {
        N_("Curvature 2"), GWY_GRAIN_CURVATURE2,
        {
            GWY_GRAIN_GROUP_CURVATURE,
            "<i>κ</i><sub>2</sub>", "kappa_2",
            -1, 0, GWY_GRAIN_SAME_UNITS,
        }
    },
    {
        N_("Curvature angle 1"), GWY_GRAIN_CURVATURE_ANGLE1,
        {
            GWY_GRAIN_GROUP_CURVATURE,
            "<i>φ</i><sub>1</sub>", "phi_1",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Curvature angle 2"), GWY_GRAIN_CURVATURE_ANGLE2,
        {
            GWY_GRAIN_GROUP_CURVATURE,
            "<i>φ</i><sub>2</sub>", "phi_2",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
    {
        N_("Major semiaxis of equivalent ellipse"), GWY_GRAIN_EQUIV_ELLIPSE_MAJOR,
        {
            GWY_GRAIN_GROUP_MOMENT,
            "<i>a</i><sub>e1</sub>", "a_e1",
            1, 0, 0,
        }
    },
    {
        N_("Minor semiaxis of equivalent ellipse"), GWY_GRAIN_EQUIV_ELLIPSE_MINOR,
        {
            GWY_GRAIN_GROUP_MOMENT,
            "<i>a</i><sub>e2</sub>", "a_e2",
            1, 0, 0,
        }
    },
    {
        N_("Orientation of equivalent ellipse"), GWY_GRAIN_EQUIV_ELLIPSE_ANGLE,
        {
            GWY_GRAIN_GROUP_MOMENT,
            "<i>φ</i><sub>e1</sub>", "phi_e1",
            0, 0, GWY_GRAIN_IS_ANGLE,
        }
    },
};

static GwyResourceClass *parent_class = NULL;
static GwyInventory *inventory = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyGrainValue, gwy_grain_value, GWY_TYPE_RESOURCE,
                        G_ADD_PRIVATE(GwyGrainValue))

static void
gwy_grain_value_class_init(GwyGrainValueClass *klass)
{
    GwyResourceClass *res_class = GWY_RESOURCE_CLASS(klass);

    parent_class = gwy_grain_value_parent_class;

    res_class->item_type = *gwy_resource_class_get_item_type(parent_class);
    res_class->item_type.type = G_TYPE_FROM_CLASS(klass);
    res_class->item_type.copy = resource_copy;
    res_class->item_type.compare = NULL;

    res_class->name = "grainvalues";
    res_class->inventory = inventory = gwy_inventory_new(&res_class->item_type);
    res_class->dump = resource_dump;
    res_class->parse = resource_parse;
    res_class->setup_builtins = resource_setup_builtins;
}

static void
gwy_grain_value_init(GwyGrainValue *gvalue)
{
    GwyGrainValuePrivate *priv;

    priv = gvalue->priv = gwy_grain_value_get_instance_private(gvalue);
    gwy_clear1(priv->data);
}

static void
resource_setup_builtins(GwyInventory *res_inventory)
{
    for (guint i = 0; i < G_N_ELEMENTS(grain_values); i++) {
        GwyGrainValue *gvalue = create_grain_value(grain_values[i].name, &grain_values[i].data, TRUE);
        gvalue->priv->builtin = grain_values[i].quantity;
        gwy_inventory_insert_item(res_inventory, gvalue);
        g_object_unref(gvalue);
    }
}

static guint
remove_nonidentifier_characters(char *s)
{
    gboolean have_letter = FALSE;
    guint i, j;

    for (i = j = 0; s[i]; i++) {
        if (have_letter && g_ascii_isalnum(s[i]))
            s[j++] = s[i];
        else if (g_ascii_isalpha(s[i])) {
            s[j++] = s[i];
            have_letter = TRUE;
        }
    }
    s[j] = '\0';

    return strlen(s);
}

static void
gwy_grain_value_data_sanitize(GwyGrainValueData *data,
                              gchar *symbol, gchar *symbol_markup)
{
    if (!gwy_ascii_strisident(symbol, "_", NULL)) {
        g_warning("Grain value symbol is not an identifier.");
        if (!remove_nonidentifier_characters(symbol))
            data->symbol = "v";
    }
    if (!data->symbol)
        data->symbol = g_intern_string(symbol);

    /* FIXME: Cannot validate symbol markup without Pango. */
    if (symbol_markup && g_utf8_validate(symbol_markup, -1, NULL))
        data->symbol_markup = g_intern_string(symbol_markup);
    else {
        g_warning("Grain value markup is not valid UTF-8.");
        data->symbol_markup = data->symbol;
    }

    if (ABS(data->power_xy) > 12) {
        g_warning("Too large xy power for grain value.");
        data->power_xy = CLAMP(data->power_xy, -12, 12);
    }
    if (ABS(data->power_z) > 12) {
        g_warning("Too large z power for grain value.");
        data->power_z = CLAMP(data->power_z, -12, 12);
    }
}

gpointer
resource_copy(gpointer item)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(item), NULL);

    GwyGrainValue *gvalue = GWY_GRAIN_VALUE(item);
    GwyGrainValue *copy = create_grain_value(gwy_resource_get_name(GWY_RESOURCE(item)), &gvalue->priv->data, FALSE);

    return copy;
}

static GwyGrainValue*
create_grain_value(const gchar *name,
                   const GwyGrainValueData *data,
                   gboolean is_const)
{
    GwyGrainValue *gvalue = g_object_new(GWY_TYPE_GRAIN_VALUE,
                                         "name", name,
                                         "const", is_const,
                                         NULL);
    gvalue->priv->data = *data;

    return gvalue;
}

static void
resource_dump(GwyResource *resource, GString *str)
{
    g_return_if_fail(GWY_IS_GRAIN_VALUE(resource));

    GwyGrainValue *gvalue = GWY_GRAIN_VALUE(resource);
    GwyGrainValuePrivate *priv = gvalue->priv;
    const GwyGrainValueData *data = &priv->data;
    g_return_if_fail(data->group == GWY_GRAIN_GROUP_USER);

    /* Information */
    g_string_append_printf(str,
                           "symbol %s\n"
                           "symbol_markup %s\n"
                           "power_xy %d\n"
                           "power_z %d\n"
                           "%s"
                           "%s"
                           "expression %s\n",
                           data->symbol,
                           data->symbol_markup,
                           data->power_xy,
                           data->power_z,
                           (data->flags & GWY_GRAIN_SAME_UNITS) ? "same_units 1\n" : "",
                           (data->flags & GWY_GRAIN_IS_ANGLE) ? "is_angle 1\n" : "",
                           priv->expression);
}

static gpointer
ensure_builtin_names(G_GNUC_UNUSED gpointer arg)
{
    const gchar **names = g_new(const gchar*, N_GRAIN_QUANTITIES);
    for (guint i = 0; i < N_GRAIN_QUANTITIES; i++) {
        GwyGrainValue *gvalue;
        if ((gvalue = gwy_grain_values_get_builtin_grain_value(i)))
            names[i] = gvalue->priv->data.symbol;
        else
            names[i] = "";  /* Impossible variable name */
    }

    return names;
}

static gboolean
resolve_expressions(const gchar *expression,
                    guint *vars,
                    GwyExpr *expr,
                    GError **error)
{
    static GOnce builtin_names_once = G_ONCE_INIT;
    GwyExpr *freeexpr = NULL;
    guint *freevars = NULL;
    const gchar* const *names;
    gboolean ok;

    if (!expr)
        expr = freeexpr = gwy_expr_new();

    if (!gwy_expr_compile(expr, expression, error)) {
        if (freeexpr)
            gwy_expr_free(freeexpr);
        return FALSE;
    }

    if (!vars)
        vars = freevars = g_new(guint, N_GRAIN_QUANTITIES);

    names = g_once(&builtin_names_once, ensure_builtin_names, NULL);
    ok = !gwy_expr_resolve_variables(expr, N_GRAIN_QUANTITIES, names, vars);

    g_free(freevars);
    if (freeexpr)
        gwy_expr_free(freeexpr);

    return ok;
}

static GwyResource*
resource_parse(const gchar *text)
{
    GwyGrainValue *gvalue = NULL;
    GwyGrainValueData data;
    gchar *str, *p, *line, *key, *value, *expression = NULL, *symbol = NULL, *symbol_markup = NULL;

    g_return_val_if_fail(text, NULL);
    GwyGrainValueClass *klass = g_type_class_peek(GWY_TYPE_GRAIN_VALUE);
    g_return_val_if_fail(klass, NULL);

    gwy_clear1(data);
    data.group = GWY_GRAIN_GROUP_USER;

    p = str = g_strdup(text);
    while ((line = gwy_str_next_line(&p))) {
        g_strstrip(line);
        if (!line[0] || line[0] == '#')
            continue;

        key = line;
        value = strchr(key, ' ');
        if (value) {
            *value = '\0';
            value++;
            g_strstrip(value);
        }
        if (!value || !*value) {
            g_warning("Missing value for `%s'.", key);
            continue;
        }

        if (gwy_strequal(key, "symbol") && strlen(value))
            gwy_assign_string(&symbol, value);
        else if (gwy_strequal(key, "symbol_markup") && strlen(value))
            gwy_assign_string(&symbol_markup, value);
        else if (gwy_strequal(key, "power_xy"))
            data.power_xy = atoi(value);
        else if (gwy_strequal(key, "power_z"))
            data.power_z = atoi(value);
        else if (gwy_strequal(key, "same_units"))
            data.flags |= GWY_GRAIN_SAME_UNITS*(!!atoi(value));
        else if (gwy_strequal(key, "is_angle"))
            data.flags |= GWY_GRAIN_IS_ANGLE*(!!atoi(value));
        else if (gwy_strequal(key, "expression"))
            expression = value;
        else
            g_warning("Unknown field `%s'.", key);
    }

    if (symbol && resolve_expressions(expression, NULL, NULL, NULL)) {
        gvalue = create_grain_value("", &data, FALSE);
        gwy_grain_value_data_sanitize(&gvalue->priv->data, symbol, symbol_markup);
        gvalue->priv->expression = g_strdup(expression);
    }

    g_free(symbol);
    g_free(symbol_markup);
    g_free(str);

    return (GwyResource*)gvalue;
}

/**
 * gwy_grain_value_get_group:
 * @gvalue: A grain value object.
 *
 * Gets the group of a grain value.
 *
 * All user-defined grain values belong to %GWY_GRAIN_GROUP_USER group, built-in grain values belong to other
 * groups.
 *
 * Returns: The group of @gvalue.
 **/
GwyGrainValueGroup
gwy_grain_value_get_group(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), 0);
    return gvalue->priv->data.group;
}

/**
 * gwy_grain_value_get_symbol_markup:
 * @gvalue: A grain value object.
 *
 * Gets the rich text symbol representing a grain value.
 *
 * The returned value can contain Pango markup and is suitable for instance for graph axis labels.
 *
 * Returns: Rich text symbol of @gvalue, owned by @gvalue.
 **/
const gchar*
gwy_grain_value_get_symbol_markup(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), NULL);
    return gvalue->priv->data.symbol_markup;
}

/**
 * gwy_grain_value_set_symbol_markup:
 * @gvalue: A grain value object.
 * @symbol: The new symbol.
 *
 * Sets the rich text symbol representing a grain value.
 *
 * See gwy_grain_value_get_symbol_markup() for details.
 **/
void
gwy_grain_value_set_symbol_markup(GwyGrainValue *gvalue,
                                  const gchar *symbol)
{
    g_return_if_fail(GWY_IS_GRAIN_VALUE(gvalue));
    GwyGrainValuePrivate *priv = gvalue->priv;
    g_return_if_fail(priv->data.group == GWY_GRAIN_GROUP_USER);
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gvalue)));

    if (priv->data.symbol_markup == symbol || (priv->data.symbol_markup
                                               && symbol
                                               && gwy_strequal(priv->data.symbol_markup, symbol)))
        return;

    priv->data.symbol_markup = g_intern_string(symbol);
    gwy_resource_data_changed(GWY_RESOURCE(gvalue));
}

/**
 * gwy_grain_value_get_symbol:
 * @gvalue: A grain value object.
 *
 * Gets the plain symbol representing a grain value.
 *
 * The plain symbol is used in expressions.  It has to be a valid identifier and it should be easy to type.  (Note
 * currently it is not possible to use user-defined grain quantities in expressions for other user-defined grain
 * quantities.)
 *
 * Returns: Plain symbol of @gvalue, owned by @gvalue.
 **/
const gchar*
gwy_grain_value_get_symbol(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), NULL);
    return gvalue->priv->data.symbol;
}

/**
 * gwy_grain_value_set_symbol:
 * @gvalue: A grain value object.
 * @symbol: The new symbol.
 *
 * Sets the plain symbol representing a grain value.
 *
 * See gwy_grain_value_get_symbol() for details.
 **/
void
gwy_grain_value_set_symbol(GwyGrainValue *gvalue,
                           const gchar *symbol)
{
    g_return_if_fail(GWY_IS_GRAIN_VALUE(gvalue));
    GwyGrainValuePrivate *priv = gvalue->priv;
    g_return_if_fail(priv->data.group == GWY_GRAIN_GROUP_USER);
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gvalue)));
    g_return_if_fail(!symbol || !*symbol || gwy_ascii_strisident(symbol, "_", NULL));

    if (priv->data.symbol == symbol || (priv->data.symbol
                                        && symbol
                                        && gwy_strequal(priv->data.symbol, symbol)))
        return;

    priv->data.symbol = g_intern_string(symbol);
    gwy_resource_data_changed(GWY_RESOURCE(gvalue));
}

/**
 * gwy_grain_value_get_power_xy:
 * @gvalue: A grain value object.
 *
 * Gets the power of lateral dimensions in a grain value.
 *
 * The units of a grain value are determined as the product of lateral units and value units, raised to certain
 * powers.  For instance lengths in the horizontal plane have xy power of 1 and areas have 2, whereas volumes have xy
 * power of 2 and value power of 1.
 *
 * Returns: The power of lateral dimensions.
 **/
gint
gwy_grain_value_get_power_xy(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), 0);
    return gvalue->priv->data.power_xy;
}

/**
 * gwy_grain_value_set_power_xy:
 * @gvalue: A grain value object.
 * @power_xy: The new lateral dimensions power.
 *
 * Sets the power of lateral dimensions in a grain value.
 **/
void
gwy_grain_value_set_power_xy(GwyGrainValue *gvalue,
                             gint power_xy)
{
    g_return_if_fail(GWY_IS_GRAIN_VALUE(gvalue));
    GwyGrainValuePrivate *priv = gvalue->priv;
    g_return_if_fail(priv->data.group == GWY_GRAIN_GROUP_USER);
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gvalue)));

    if (priv->data.power_xy == power_xy)
        return;

    priv->data.power_xy = power_xy;
    gwy_resource_data_changed(GWY_RESOURCE(gvalue));
}

/**
 * gwy_grain_value_get_power_z:
 * @gvalue: A grain value object.
 *
 * Gets the power of value (height) in a grain value.
 *
 * See gwy_grain_value_get_power_xy() for details.
 *
 * Returns: The power of value (height).
 **/
gint
gwy_grain_value_get_power_z(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), 0);
    return gvalue->priv->data.power_z;
}

/**
 * gwy_grain_value_set_power_z:
 * @gvalue: A grain value object.
 * @power_z: The new value (height) power.
 *
 * Sets the power of value (height) in a grain value.
 **/
void
gwy_grain_value_set_power_z(GwyGrainValue *gvalue,
                            gint power_z)
{
    g_return_if_fail(GWY_IS_GRAIN_VALUE(gvalue));
    GwyGrainValuePrivate *priv = gvalue->priv;
    g_return_if_fail(priv->data.group == GWY_GRAIN_GROUP_USER);
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gvalue)));

    if (priv->data.power_z == power_z)
        return;

    priv->data.power_z = power_z;
    gwy_resource_data_changed(GWY_RESOURCE(gvalue));
}

/**
 * gwy_grain_value_get_flags:
 * @gvalue: A grain value object.
 *
 * Obtains the special attributes of a grain quantity.
 *
 * Returns: The special attribute flags set on @gvalue.  See
 *          #GwyGrainValueFlags for their description.
 **/
GwyGrainValueFlags
gwy_grain_value_get_flags(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), 0);
    return gvalue->priv->data.flags;
}

/**
 * gwy_grain_value_set_flags:
 * @gvalue: A grain value object.
 * @flags: The special attributes to set on @gvalue.
 *
 * Sets the special attributes of a grain quantity.
 *
 * The value of @flags determines the complete new set of flags.  To set individual flags, obtain the current set with
 * gwy_grain_value_get_flags() first and then set/unset individual flags.
 **/
void
gwy_grain_value_set_flags(GwyGrainValue *gvalue,
                          GwyGrainValueFlags flags)
{
    g_return_if_fail(GWY_IS_GRAIN_VALUE(gvalue));
    GwyGrainValuePrivate *priv = gvalue->priv;
    g_return_if_fail(priv->data.group == GWY_GRAIN_GROUP_USER);
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gvalue)));

    if (priv->data.flags == flags)
        return;

    priv->data.flags = flags;
    gwy_resource_data_changed(GWY_RESOURCE(gvalue));
}

/**
 * gwy_grain_value_get_quantity:
 * @gvalue: A grain value object.
 *
 * Gets the built-in grain quantity corresponding to a grain value.
 *
 * Returns: The corresponding built-in #GwyGrainQuantity if @gvalue is a built-it grain value, -1 if @gvalue is an
 *          user-defined grain value.
 **/
GwyGrainQuantity
gwy_grain_value_get_quantity(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), -1);
    GwyGrainValuePrivate *priv = gvalue->priv;

    if (priv->data.group == GWY_GRAIN_GROUP_USER)
        return -1;

    return priv->builtin;
}

/**
 * gwy_grain_value_get_expression:
 * @gvalue: A grain value object.
 *
 * Gets the expression of a user-defined grain value.
 *
 * Returns: The expression as a string owned by @gvalue, %NULL if @gvalue is a built-in grain value.
 **/
const gchar*
gwy_grain_value_get_expression(GwyGrainValue *gvalue)
{
    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), NULL);
    GwyGrainValuePrivate *priv = gvalue->priv;

    if (priv->data.group != GWY_GRAIN_GROUP_USER)
        return NULL;
    return priv->expression;
}

/**
 * gwy_grain_value_set_expression:
 * @gvalue: A grain value object.
 * @expression: New grain value expression.
 * @error: Return location for the error, or %NULL to ignore errors.
 *
 * Sets the expression of a user-defined grain value.
 *
 * It is an error to call this function on a built-in quantity.
 *
 * Returns: %TRUE if the expression is compilable and references only known grain quantities.  %FALSE is the
 *          expression is not calculable, in this case the @gvalue's expression is unchanged.
 **/
gboolean
gwy_grain_value_set_expression(GwyGrainValue *gvalue,
                               const gchar *expression,
                               GError **error)
{
    GError *err = NULL;

    g_return_val_if_fail(GWY_IS_GRAIN_VALUE(gvalue), FALSE);
    GwyGrainValuePrivate *priv = gvalue->priv;
    g_return_val_if_fail(priv->data.group == GWY_GRAIN_GROUP_USER, FALSE);
    g_return_val_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gvalue)), FALSE);

    if (expression == priv->expression)
        return TRUE;

    if (!resolve_expressions(expression, NULL, NULL, &err)) {
        if (err)
            g_propagate_error(error, err);
        else
            g_set_error(error, GWY_EXPR_ERROR, GWY_EXPR_ERROR_UNRESOLVED_IDENTIFIERS,
                        "Unresolved identifiers");
        return FALSE;
    }

    gwy_assign_string(&priv->expression, expression);
    gwy_resource_data_changed(GWY_RESOURCE(gvalue));

    return TRUE;
}

/**
 * gwy_grain_value_group_name:
 * @group: Grain value group.
 *
 * Obtains the name of a grain value group.
 *
 * Returns: The grain value group name as a constant untranslated string,
 *          owned by the library.
 **/
const gchar*
gwy_grain_value_group_name(GwyGrainValueGroup group)
{
    static const gchar *group_names[] = {
        N_("Id"),
        N_("Position"),
        N_("Value"),
        N_("Area"),
        N_("Volume"),
        N_("Boundary"),
        N_("Slope"),
        N_("Curvature"),
        N_("Moment"),
    };

    if (group == GWY_GRAIN_GROUP_USER)
        return N_("User");

    g_return_val_if_fail(group < G_N_ELEMENTS(group_names), NULL);
    return group_names[group];
}

/**
 * gwy_grain_values:
 *
 * Gets the inventory with all the grain values.
 *
 * Returns: (transfer none): Grain value inventory.
 **/
GwyInventory*
gwy_grain_values(void)
{
    return inventory;
}

/**
 * gwy_grain_values_get_grain_value:
 * @name: Grain quantity name.
 *
 * Convenience function to get a grain quantity from gwy_grain_values() by name.
 *
 * Returns: (transfer none): Grain quantity identified by @name or %NULL if there is no such grain quantity.
 **/
GwyGrainValue*
gwy_grain_values_get_grain_value(const gchar *name)
{
    g_return_val_if_fail(inventory, NULL);
    return (GwyGrainValue*)gwy_inventory_get_item(inventory, name);
}

/**
 * gwy_grain_values_get_grain_value_by_symbol:
 * @symbol: Grain quantity symbol.
 *
 * Convenience function to get a grain quantity from gwy_grain_values() by its symbol.
 *
 * Returns: (transfer none): Grain quantity with symbol @symbol or %NULL if there is no such grain quantity.
 **/
GwyGrainValue*
gwy_grain_values_get_grain_value_by_symbol(const gchar *symbol)
{
    g_return_val_if_fail(symbol, NULL);

    guint n = gwy_inventory_get_n_items(inventory);
    for (guint k = 0; k < n; k++) {
        GwyGrainValue *gvalue = gwy_inventory_get_nth_item(inventory, k);
        if (gwy_strequal(gwy_grain_value_get_symbol(gvalue), symbol))
            return gvalue;
    }

    return NULL;
}

static gboolean
find_grain_value_by_quantity(G_GNUC_UNUSED gpointer key,
                             gpointer value,
                             gpointer user_data)
{
    GwyGrainValue *gvalue = (GwyGrainValue*)value;
    GwyGrainValuePrivate *priv = gvalue->priv;

    return (priv->data.group != GWY_GRAIN_GROUP_USER && priv->builtin == GPOINTER_TO_UINT(user_data));
}

/**
 * gwy_grain_values_get_builtin_grain_value:
 * @quantity: A #GwyGrainQuantity value.
 *
 * Obtains the built-in grain value corresponding to given enum value.
 *
 * Returns: (transfer none):
 *          The built-in grain value corresponding to @quantity, %NULL if there is no such grain value.
 **/
GwyGrainValue*
gwy_grain_values_get_builtin_grain_value(GwyGrainQuantity quantity)
{
    return gwy_inventory_find(inventory, find_grain_value_by_quantity, GUINT_TO_POINTER(quantity));
}

/**
 * gwy_nield_grain_values:
 * @nield: Number field with numbered grains.
 * @field: Data field used for marking.  For some values its values are not used, but its dimensions determine
 *         the dimensions of @grains.
 * @gvalues: Array of grain value objects.
 * @nvalues: Number of items in @gvalues.
 * @results: Array of length @nvalues of arrays of length @ngrains+1 of doubles to put the calculated values to.
 *
 * Calculates a set of grain values.
 *
 * See also gwy_field_grains_get_quantities() for a simplier function for built-in grain values.
 **/
void
gwy_nield_grain_values(GwyNield *nield,
                       GwyField *field,
                       GwyGrainValue **gvalues,
                       guint nvalues,
                       gdouble **results)
{
    if (!_gwy_nield_check_field(nield, field, FALSE) || !nvalues)
        return;
    g_return_if_fail(gvalues);
    g_return_if_fail(results);

    gboolean duplicates = FALSE;
    gint ngrains = gwy_nield_max(nield);

    /* Builtins. */
    gdouble *quantities[N_GRAIN_QUANTITIES];
    gwy_clear(quantities, N_GRAIN_QUANTITIES);
    for (guint i = 0; i < nvalues; i++) {
        GwyGrainValue *gvalue = gvalues[i];
        g_return_if_fail(GWY_IS_GRAIN_VALUE(gvalue));
        GwyGrainValuePrivate *priv = gvalue->priv;

        if (priv->data.group == GWY_GRAIN_GROUP_USER)
            continue;

        guint q = priv->builtin;
        g_assert(q < N_GRAIN_QUANTITIES);
        if (quantities[q])
            duplicates = TRUE;
        else
            quantities[q] = results[i];
    }

    /* Expressions (user quantities). */
    GList *buffers = NULL;
    guint vars[N_GRAIN_QUANTITIES];
    GwyExpr *expr = gwy_expr_new();
    for (guint i = 0; i < nvalues; i++) {
        GwyGrainValue *gvalue = gvalues[i];
        GwyGrainValuePrivate *priv = gvalue->priv;
        if (priv->data.group != GWY_GRAIN_GROUP_USER)
            continue;

        gboolean resolved = resolve_expressions(priv->expression, vars, expr, NULL);
        g_return_if_fail(resolved);
        for (guint q = 0; q < N_GRAIN_QUANTITIES; q++) {
            if (vars[q] && !quantities[q]) {
                quantities[q] = g_new(gdouble, ngrains + 1);
                buffers = g_list_prepend(buffers, quantities[q]);
            }
        }
    }

    /* Pack into a flat array */
    GwyGrainQuantity packed_builtins[N_GRAIN_QUANTITIES];
    gdouble *packed_results[N_GRAIN_QUANTITIES];
    guint npacked = 0;
    for (guint i = 0; i < N_GRAIN_QUANTITIES; i++) {
        if (quantities[i]) {
            packed_builtins[npacked] = i;
            packed_results[npacked] = quantities[i];
            npacked++;
        }
    }

    /* Calculate the built-in quantities */
    gwy_nield_grain_quantities(nield, field, packed_builtins, npacked, packed_results);

    /* Calculate the user quantities */
    for (guint i = 0; i < nvalues; i++) {
        GwyGrainValue *gvalue = gvalues[i];
        GwyGrainValuePrivate *priv = gvalue->priv;
        if (priv->data.group != GWY_GRAIN_GROUP_USER)
            continue;

        resolve_expressions(priv->expression, vars, expr, NULL);
        gdouble *mapped[N_GRAIN_QUANTITIES];
        gwy_clear(mapped, N_GRAIN_QUANTITIES);
        for (guint q = 0; q < N_GRAIN_QUANTITIES; q++) {
            if (vars[q]) {
                g_assert(quantities[q]);
                mapped[vars[q]] = quantities[q];
            }
        }
        gwy_expr_vector_execute(expr, ngrains+1, (const gdouble**)mapped, results[i]);
    }

    /* Copy duplicates to all instances in @results. */
    if (duplicates) {
        for (guint i = 1; i < nvalues; i++) {
            GwyGrainValue *gvalue = gvalues[i];
            guint j;
            for (j = 0; j < i; j++) {
                if (gvalues[j] == gvalue)
                    break;
            }
            if (j == i)
                continue;

            memcpy(results[i], results[j], (ngrains + 1)*sizeof(gdouble));
        }
    }

    /* Free */
    for (GList *l = buffers; l; l = g_list_next(l))
        g_free(l->data);
    g_list_free(buffers);
    gwy_expr_free(expr);
}

/**
 * SECTION: grain-value
 * @title: GwyGrainValue
 * @short_description: Grain value resource type
 **/

/**
 * GwyGrainValueFlags:
 * @GWY_GRAIN_SAME_UNITS: Certain grain quantities, such as the surface area or absolute inclination, are only
 *                              meaningful if value (height) is the same physical quantity as lateral dimensions.
 *                              These have this flag set to %TRUE.
 * @GWY_GRAIN_IS_ANGLE: Angular quantities are internally always calculated in radians, however they can be
 *                            presented in degrees in the user interface. To enable such special handling, set this
 *                            flag to %TRUE.
 *
 * Special attributes of grain values.
 **/

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