/*
 *  $Id: selection.c 29725 2026-04-03 07:03:21Z 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/serialize.h"
#include "libgwyddion/serializable-utils.h"
#include "libgwyddion/selection.h"

enum {
    PROP_0,
    PROP_MAX_OBJECTS,
    NUM_PROPERTIES,
};

enum {
    SGNL_CHANGED,
    SGNL_FINISHED,
    NUM_SIGNALS,
};

enum {
    ITEM_MAX, ITEM_DATA,
    NUM_ITEMS
};

struct _GwySelectionPrivate {
    GArray *coords;
    guint max_objects;

    /* Remember the last changed object coords, provided a single object has been changed. */
    gboolean have_last_xy;
    gdouble *last_xy;
};

struct _GwySelectionClassPrivate {
    gint dummy;
};

static void             finalize              (GObject *object);
static void             set_property          (GObject *object,
                                               guint prop_id,
                                               const GValue *value,
                                               GParamSpec *pspec);
static void             get_property          (GObject *object,
                                               guint prop_id,
                                               GValue *value,
                                               GParamSpec *pspec);
static void             serializable_init     (GwySerializableInterface *iface);
static void             serializable_itemize  (GwySerializable *serializable,
                                               GwySerializableGroup *group);
static gboolean         serializable_construct(GwySerializable *serializable,
                                               GwySerializableGroup *group,
                                               GwyErrorList **error_list);
static GwySerializable* serializable_copy     (GwySerializable *serializable);
static void             serializable_assign   (GwySerializable *destination,
                                               GwySerializable *source);
static void             crop_default          (GwySelection *selection,
                                               gdouble xmin,
                                               gdouble ymin,
                                               gdouble xmax,
                                               gdouble ymax);
static void             move_default          (GwySelection *selection,
                                               gdouble vx,
                                               gdouble vy);
static const gchar*     coord_symbol_default  (GwySelection *selection,
                                               gint i);
static gdouble*         remember_last_xy      (GwySelection *selection,
                                               gint i);

static guint signals[NUM_SIGNALS];
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GObjectClass *parent_class = NULL;

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "max",  .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "data", .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                      .flags = GWY_SERIALIZABLE_BIG_DATA      },
};

G_DEFINE_TYPE_EXTENDED(GwySelection, gwy_selection, G_TYPE_OBJECT, G_TYPE_FLAG_ABSTRACT,
                       G_ADD_PRIVATE(GwySelection)
                       G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
gwy_selection_class_init(GwySelectionClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_selection_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->get_property = get_property;
    gobject_class->set_property = set_property;

    klass->crop = crop_default;
    klass->move = move_default;
    klass->coord_symbol = coord_symbol_default;

    /**
     * GwySelection::changed:
     * @gwyselection: The #GwySelection which received the signal.
     * @arg1: Changed object position hint.  If the value is nonnegative, only this object has changed.  If it's
     *        negative, the selection has to be treated as completely changed.
     *
     * The ::changed signal is emitted whenever selection changes.
     **/
    signals[SGNL_CHANGED] = g_signal_new("changed", type,
                                         G_SIGNAL_RUN_FIRST,
                                         G_STRUCT_OFFSET(GwySelectionClass, changed),
                                         NULL, NULL,
                                         g_cclosure_marshal_VOID__INT,
                                         G_TYPE_NONE, 1, G_TYPE_INT);
    g_signal_set_va_marshaller(signals[SGNL_CHANGED], type, g_cclosure_marshal_VOID__INTv);

    /**
     * GwySelection::finished:
     * @gwyselection: The #GwySelection which received the signal.
     *
     * The ::finished signal is emitted when selection is finished.
     *
     * What exactly finished means is defined by corresponding #GwyVectorLayer, but normally it involves user stopped
     * changing a selection object. Selections never emit this signal themselves.
     **/
    signals[SGNL_FINISHED] = g_signal_new("finished", type,
                                          G_SIGNAL_RUN_FIRST,
                                          G_STRUCT_OFFSET(GwySelectionClass, finished),
                                          NULL, NULL,
                                          g_cclosure_marshal_VOID__VOID,
                                          G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_FINISHED], type, g_cclosure_marshal_VOID__VOIDv);

    properties[PROP_MAX_OBJECTS] = g_param_spec_uint("max-objects", NULL,
                                                     "Maximum number of objects that can be selected",
                                                     0, 65536, 1,
                                                     GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->construct = serializable_construct;
    iface->copy      = serializable_copy;
    iface->assign    = serializable_assign;

    serializable_items[ITEM_MAX].aux.pspec = g_param_spec_int(serializable_items[ITEM_MAX].name, NULL, NULL,
                                                              0, 65536, 1,
                                                              G_PARAM_STATIC_STRINGS);
    gwy_fill_serializable_defaults_pspec(serializable_items, NUM_ITEMS, FALSE);
}

static void
gwy_selection_init(GwySelection *selection)
{
    GwySelectionPrivate *priv;

    priv = selection->priv = gwy_selection_get_instance_private(selection);
    priv->max_objects = 1;
    priv->coords = g_array_new(FALSE, FALSE, sizeof(gdouble));
}

static void
finalize(GObject *object)
{
    GwySelectionPrivate *priv = GWY_SELECTION(object)->priv;

    g_array_free(priv->coords, TRUE);
    g_free(priv->last_xy);
    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwySelection *selection = GWY_SELECTION(object);

    switch (prop_id) {
        case PROP_MAX_OBJECTS:
        gwy_selection_set_max_objects(selection, g_value_get_uint(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwySelection *selection = GWY_SELECTION(object);

    switch (prop_id) {
        case PROP_MAX_OBJECTS:
        g_value_set_uint(value, gwy_selection_get_max_objects(selection));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

/**
 * gwy_selection_get_object_size:
 * @selection: A selection.
 *
 * Gets the number of coordinates that make up a one selection object.
 *
 * Returns: The number of coordinates in one selection object.
 **/
guint
gwy_selection_get_object_size(GwySelection *selection)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), 0);
    return GWY_SELECTION_GET_CLASS(selection)->object_size;
}

/**
 * gwy_selection_clear:
 * @selection: A selection.
 *
 * Clears a selection.
 **/
void
gwy_selection_clear(GwySelection *selection)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    GwySelectionPrivate *priv = selection->priv;
    GArray *coords = priv->coords;
    if (coords->len) {
        guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
        /* Detect single-object changes. */
        if (coords->len == object_size)
            remember_last_xy(selection, 0);
        g_array_set_size(coords, 0);
        g_signal_emit(selection, signals[SGNL_CHANGED], 0, -1);
    }
}

/**
 * gwy_selection_crop:
 * @selection: A selection.
 * @xmin: Minimum x-coordinate.
 * @ymin: Minimum y-coordinate.
 * @xmax: Maximum x-coordinate.
 * @ymax: Maximum y-coordinate.
 *
 * Limits objects in a selection to a rectangle.
 *
 * Objects that are fully outside specified rectangle are removed.  Objects partially outside may be removed or cut,
 * depending on what makes sense for the specific selection type.  If the selection class does not implement this
 * method then all objects are removed.
 **/
void
gwy_selection_crop(GwySelection *selection,
                   gdouble xmin,
                   gdouble ymin,
                   gdouble xmax,
                   gdouble ymax)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    GWY_SELECTION_GET_CLASS(selection)->crop(selection, xmin, ymin, xmax, ymax);
}

/**
 * gwy_selection_move:
 * @selection: A selection.
 * @vx: Value to add to all x-coordinates.
 * @vy: Value to add to all y-coordinates.
 *
 * Moves entire selection in plane by given vector.
 *
 * If a selection class does not implement this operation the selection remains unchanged.  Bult-in selection classes
 * generally implement this operation if it is meaningful.  For some, such as #GwySelectionLattice, it is not
 * meaningful and moving #GwySelectionLattice thus does not do anything.
 **/
void
gwy_selection_move(GwySelection *selection,
                   gdouble vx,
                   gdouble vy)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    GWY_SELECTION_GET_CLASS(selection)->move(selection, vx, vy);
}

/**
 * gwy_selection_move_objects:
 * @selection: A selection.
 * @v: Array of shifts for individual coodinates
 *
 * Translates cooordinates in a selection according to a repeating pattern.
 *
 * This function is mainly useful as a helper for #GwySelection subclasses. The array @v must have
 * gwy_selection_get_object_size() values. Each coordinate in every object is translated by the corresponding values in
 * @v.
 *
 * In order to implement move(), a subclass can fill @v with the corresponding x and y shift pattern and call
 * gwy_selection_move_objects() to apply them to all objects.
 **/
void
gwy_selection_move_objects(GwySelection *selection,
                           const gdouble *v)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    g_return_if_fail(v);
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GArray *coords = selection->priv->coords;
    guint n = coords->len/object_size;
    gdouble *seldata = &g_array_index(coords, gdouble, 0);
    for (guint i = 0; i < n; i++) {
        for (guint k = 0; k < object_size; k++)
            seldata[i*object_size + k] += v[k];
    }
    g_signal_emit(selection, signals[SGNL_CHANGED], 0, -1);
}

/**
 * gwy_selection_filter:
 * @selection: A selection.
 * @filter: (scope call):
 *          Function returning %TRUE for objects that should be kept, %FALSE for objects that should be removed.
 * @data: User data passed to @filter.
 *
 * Removes selection objects matching certain criteria.
 **/
void
gwy_selection_filter(GwySelection *selection,
                     GwySelectionFilterFunc filter,
                     gpointer data)
{
    GArray *newcoords = g_array_new(FALSE, FALSE, sizeof(gdouble));
    guint object_size = gwy_selection_get_object_size(selection);
    GArray *coords = selection->priv->coords;
    guint len = coords->len/object_size;
    for (guint isrc = 0; isrc < len; isrc++) {
        if (filter(selection, isrc, data)) {
            const gdouble *srcobj = &g_array_index(coords, gdouble, object_size*isrc);
            g_array_append_vals(newcoords, srcobj, object_size);
        }
    }
    /* This is the only place we emit a signal on @selection. And do it only if we removed anything. */
    if (newcoords->len != coords->len)
        gwy_selection_set_data(selection, newcoords->len/object_size, &g_array_index(newcoords, gdouble, 0));
    g_array_free(newcoords, TRUE);
}

/**
 * gwy_selection_get_object:
 * @selection: A selection.
 * @i: Index of object to get.
 * @data: Array to store selection object data to.  Object data is an array of coordinates whose precise meaning is
 *        defined by particular selection types.
 *
 * Gets one selection object.
 *
 * Returns: %TRUE if there was such an object and @data was filled.
 **/
gboolean
gwy_selection_get_object(GwySelection *selection,
                         gint i,
                         gdouble *data)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), FALSE);
    g_return_val_if_fail(data, FALSE);

    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GArray *coords = selection->priv->coords;

    if (i < 0 || i >= coords->len/object_size)
        return FALSE;
    if (!data)
        return TRUE;

    const gdouble *selobj = &g_array_index(coords, gdouble, i*object_size);
    gwy_assign(data, selobj, object_size);
    return TRUE;
}

/**
 * gwy_selection_get_changed_object:
 * @selection: A selection.
 * @data: Array to store selection object data to.  Object data is an array of coordinates whose precise meaning is
 *        defined by particular selection types.
 *
 * Gets the previous data of the last changed selection object.
 *
 * This function can only be meaningfully used in a GwySelection::changed signal handler. Outside it always returns
 * %FALSE.
 *
 * If the @hint parameter of the signal handler is ≥ 0 and the function returns %TRUE, only a single object has been
 * changed. In such case @data will be filled with the object coordinates before the change.
 *
 * If the hint is negative and the function returns %TRUE, a single object has been deleted. In such case @data will
 * be filled with the deleted object coordinates.
 *
 * Otherwise the function returns %FALSE. In particular, it is possible that @hint ≥ 0 and the function returns
 * %FALSE. For instance when an object is added, there are no prior coordinates.
 *
 * Returns: %TRUE if @data was filled with object coordinates.
 **/
gboolean
gwy_selection_get_changed_object(GwySelection *selection,
                                 gdouble *data)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), FALSE);
    g_return_val_if_fail(data, FALSE);

    GwySelectionPrivate *priv = selection->priv;
    if (!priv->have_last_xy)
        return FALSE;

    g_assert(priv->last_xy);
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    gwy_assign(data, priv->last_xy, object_size);
    return TRUE;
}

/**
 * gwy_selection_set_object:
 * @selection: A selection.
 * @i: Index of object to set.
 * @data: Object selection data.  It's an array of coordinates whose precise meaning is defined by particular selection
 *        types.
 *
 * Sets one selection object.
 *
 * This method can be also used to append objects (if the maximum number is not exceeded).  Since there cannot be
 * holes in the object list, @i must be then equal to either the number of selected objects or special value -1
 * meaning append to end.
 *
 * Returns: The index of actually set object (useful namely when @i is -1).
 **/
gint
gwy_selection_set_object(GwySelection *selection,
                         gint i,
                         const gdouble *data)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), -1);
    g_return_val_if_fail(selection->priv->max_objects >= 1, -1);

    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GwySelectionPrivate *priv = selection->priv;
    GArray *coords = priv->coords;
    guint n = coords->len/object_size;
    if (i < 0)
        i = n;
    guint max_n = MIN(priv->max_objects-1, n);
    g_return_val_if_fail(i <= max_n, -1);
    priv->have_last_xy = FALSE;
    if (i == n)
        g_array_append_vals(coords, data, object_size);
    else {
        gdouble *selobj = &g_array_index(coords, gdouble, i*object_size);
        remember_last_xy(selection, i);
        gwy_assign(selobj, data, object_size);
    }
    g_signal_emit(selection, signals[SGNL_CHANGED], 0, i);
    priv->have_last_xy = FALSE;
    return i;
}

/**
 * gwy_selection_delete_object:
 * @selection: A selection.
 * @i: Index of object to delete.
 *
 * Deletes a one selection object.
 *
 * Since there cannot be holes in the object list, the rest of selection objects is moved to close the gap.
 **/
void
gwy_selection_delete_object(GwySelection *selection,
                            gint i)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    GwySelectionPrivate *priv = selection->priv;
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GArray *coords = priv->coords;
    guint n = coords->len/object_size;
    g_return_if_fail(i >= 0 && i < n);
    remember_last_xy(selection, i);
    g_array_remove_range(coords, i*object_size, object_size);
    /* This is kind of weird because we emit "changed" with hint < 0, but still there is only one object which has
     * changed, so the handler can deduce an object has been deleted. */
    g_signal_emit(selection, signals[SGNL_CHANGED], 0, -1);
    priv->have_last_xy = FALSE;
}

/**
 * gwy_selection_get_data:
 * @selection: A selection.
 * @data: (nullable):
 *        Array to store selection data to.  Selection data is an array of coordinates whose precise meaning is
 *        defined by particular selection types.  It may be %NULL.
 *
 * Copies all selection data to given array.
 *
 * Returns: The number of objects in the selection.  This is *not* the required size of @data, which must be at least
 *          gwy_selection_get_object_size() times larger.
 **/
gint
gwy_selection_get_data(GwySelection *selection,
                       gdouble *data)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), 0);
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GArray *coords = selection->priv->coords;
    guint n = coords->len/object_size;
    if (data && n) {
        const gdouble *seldata = &g_array_index(coords, gdouble, 0);
        gwy_assign(data, seldata, n*object_size);
    }
    return n;
}

/**
 * gwy_selection_get_n_objects:
 * @selection: A selection.
 *
 * Gets the number of objects in a selection.
 *
 * Returns: The number of objects in the selection.  The number of individual coordinates of is
 *          gwy_selection_get_object_size() times larger.
 **/
gint
gwy_selection_get_n_objects(GwySelection *selection)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), 0);
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GArray *coords = selection->priv->coords;
    return coords->len/object_size;
}

/**
 * gwy_selection_get_data_array:
 * @selection: A selection.
 *
 * Gets the entire data array of a selection for reading.
 *
 * The returned array is owned by @selection and remains valid only until the selection changes.
 *
 * Use gwy_selection_get_n_objects() to obtain the number of objects in the array.
 *
 * Returns: Data of the entire selection.
 **/
const gdouble*
gwy_selection_get_data_array(GwySelection *selection)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), NULL);
    GArray *coords = selection->priv->coords;
    return &g_array_index(coords, gdouble, 0);
}

/**
 * gwy_selection_set_data:
 * @selection: A selection.
 * @nselected: The number of selected objects.
 * @data: Selection data, that is an array @nselected * gwy_selection_get_object_size() long with selected object
 *        coordinates.
 *
 * Sets selection data.
 **/
void
gwy_selection_set_data(GwySelection *selection,
                       gint nselected,
                       const gdouble *data)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    GwySelectionPrivate *priv = selection->priv;
    guint max_objects = priv->max_objects;
    if (nselected > max_objects) {
        g_warning("nselected larger than max. number of objects");
        nselected = max_objects;
    }

    GArray *coords = priv->coords;
    if (!coords->len && !nselected)
        return;

    /* Detect single-object changes. */
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    gboolean is_single_object_change = (coords->len == object_size && nselected <= 1);
    if (is_single_object_change)
        remember_last_xy(selection, 0);

    g_array_set_size(coords, 0);
    if (nselected) {
        g_return_if_fail(data);
        g_array_append_vals(coords, data, nselected*object_size);
    }

    g_signal_emit(selection, signals[SGNL_CHANGED], 0, (is_single_object_change && nselected) ? 0 : -1);
    priv->have_last_xy = FALSE;
}

/**
 * gwy_selection_get_max_objects:
 * @selection: A selection.
 *
 * Gets the maximum number of selected objects.
 *
 * Returns: The maximum number of selected objects;
 **/
guint
gwy_selection_get_max_objects(GwySelection *selection)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), 0);
    return selection->priv->max_objects;
}

/**
 * gwy_selection_set_max_objects:
 * @selection: A selection.
 * @max_objects: The maximum number of objects allowed to select.  Note particular selection types may allow only
 *               specific values.
 *
 * Sets the maximum number of objects allowed to select.
 *
 * When selection reaches this number of selected objects, it emits "finished" signal.
 **/
void
gwy_selection_set_max_objects(GwySelection *selection,
                              guint max_objects)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    GwySelectionPrivate *priv = selection->priv;
    if (max_objects == priv->max_objects)
        return;
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GArray *coords = priv->coords;
    priv->max_objects = max_objects;
    if (max_objects < coords->len/object_size) {
        g_array_set_size(coords, max_objects*object_size);
        g_signal_emit(selection, signals[SGNL_CHANGED], 0, -1);
    }
    g_object_notify_by_pspec(G_OBJECT(selection), properties[PROP_MAX_OBJECTS]);
}

/**
 * gwy_selection_is_full:
 * @selection: A selection.
 *
 * Checks whether the maximum number of objects is selected.
 *
 * Returns: %TRUE when the maximum possible number of objects is selected, %FALSE otherwise.
 **/
gboolean
gwy_selection_is_full(GwySelection *selection)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), FALSE);
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    GArray *coords = selection->priv->coords;
    return coords->len/object_size == selection->priv->max_objects;
}

/**
 * gwy_selection_changed:
 * @selection: A selection.
 * @i: Index of object that changed.  Use -1 when not applicable, e.g., when complete selection was changed, cleared,
 *     or truncated.
 *
 * Emits "changed" signal on a selection.
 **/
void
gwy_selection_changed(GwySelection *selection,
                      gint i)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    g_signal_emit(selection, signals[SGNL_CHANGED], 0, i);
}

/**
 * gwy_selection_finished:
 * @selection: A selection.
 *
 * Emits "finished" signal on a selection.
 **/
void
gwy_selection_finished(GwySelection *selection)
{
    g_return_if_fail(GWY_IS_SELECTION(selection));
    g_signal_emit(selection, signals[SGNL_FINISHED], 0);
}

/**
 * gwy_selection_get_coord_symbol:
 * @selection: A selection.
 * @i: Index of coordinate within a selection object.
 *
 * Gets a symbol of a specific coodinate in a selection.
 *
 * The returned string is static (or can be treated as such). However, it may not be constant for a given selection.
 * If the selection is along a specific axis, like #GwySelectionAxis or #GwySelectionRange, the symbols change when
 * the orientation changes.
 *
 * Returns: The symbol as a string with infinite lifetime.
 **/
const gchar*
gwy_selection_get_coord_symbol(GwySelection *selection,
                               gint i)
{
    g_return_val_if_fail(GWY_IS_SELECTION(selection), "");
    GwySelectionClass *klass = GWY_SELECTION_GET_CLASS(selection);
    g_return_val_if_fail(i >= 0 && i < klass->object_size, "");
    return klass->coord_symbol(selection, i);
}

static void
crop_default(GwySelection *selection,
             G_GNUC_UNUSED gdouble xmin,
             G_GNUC_UNUSED gdouble ymin,
             G_GNUC_UNUSED gdouble xmax,
             G_GNUC_UNUSED gdouble ymax)
{
    /* If the selection class does not implement crop, we have to remove all objects. */
    gwy_selection_clear(selection);
}

static void
move_default(G_GNUC_UNUSED GwySelection *selection,
             G_GNUC_UNUSED gdouble vx,
             G_GNUC_UNUSED gdouble vy)
{
    /* If the selection class does not implement move we do nothing. */
}

/* Orientable (and otherwise unusual) selections need to override this. */
static const gchar*
coord_symbol_default(GwySelection *selection, gint i)
{
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    const gchar *xysymbol = (i % 2) ? "y" : "x";
    if (object_size <= 2)
        return xysymbol;

    gchar *s = g_strdup_printf("%s%u", xysymbol, i/2 + 1);
    const gchar *retval = g_intern_string(s);
    g_free(s);
    return retval;
}

static gdouble*
remember_last_xy(GwySelection *selection, gint i)
{
    GwySelectionPrivate *priv = selection->priv;
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;
    if (G_UNLIKELY(!priv->last_xy)) {
        priv->last_xy = g_new(gdouble, object_size);
    }
    priv->have_last_xy = TRUE;
    const gdouble *selobj = &g_array_index(priv->coords, gdouble, i*object_size);
    gwy_assign(priv->last_xy, selobj, object_size);
    return priv->last_xy;
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwySelection *selection = GWY_SELECTION(serializable);
    GwySelectionPrivate *priv = selection->priv;

    gwy_serializable_group_append_int32(group, serializable_items + ITEM_MAX, priv->max_objects);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_DATA,
                                               (gdouble*)priv->coords->data, priv->coords->len);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwySerializableItem its[NUM_ITEMS], *it;
    gwy_assign(its, serializable_items, NUM_ITEMS);
    const gchar *type_name = G_OBJECT_TYPE_NAME(serializable);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, type_name, error_list);
    gboolean ok = FALSE;

    GwySelection *selection = GWY_SELECTION(serializable);
    GwySelectionPrivate *priv = selection->priv;
    guint object_size = GWY_SELECTION_GET_CLASS(selection)->object_size;

    it = its + ITEM_DATA;
    if (!gwy_check_data_length_multiple(error_list, type_name, it->array_size, object_size))
        goto fail;

    g_array_set_size(priv->coords, 0);
    g_array_append_vals(priv->coords, it->value.v_double_array, it->array_size);

    /* Already validated by pspec. */
    priv->max_objects = MAX(its[ITEM_MAX].value.v_uint32, priv->coords->len/object_size);

    ok = TRUE;

fail:
    g_free(its[ITEM_DATA].value.v_double_array);
    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwySelection *selection = GWY_SELECTION(serializable);
    GwySelection *copy = g_object_new(G_OBJECT_TYPE(serializable), NULL);
    GwySelectionPrivate *priv = selection->priv, *cpriv = copy->priv;
    cpriv->max_objects = priv->max_objects;
    g_array_append_vals(cpriv->coords, priv->coords->data, priv->coords->len);
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwySelection *destselection = GWY_SELECTION(destination), *srcselection = GWY_SELECTION(source);
    GwySelectionPrivate *dpriv = destselection->priv, *spriv = srcselection->priv;
    dpriv->max_objects = spriv->max_objects;
    guint len = spriv->coords->len;
    g_array_set_size(dpriv->coords, len);
    gwy_assign(&g_array_index(dpriv->coords, gdouble, 0), &g_array_index(spriv->coords, gdouble, 0), len);
    /* NB: A subclass can chain to this method, but it need to do it at the end (not at the beginning) to make sure
     * the signal is emitted when the selection is fully in the new state. */
    g_signal_emit(destination, signals[SGNL_CHANGED], 0);
}

/**
 * gwy_selection_copy:
 * @selection: A data selection to duplicate.
 *
 * Create a new data selection as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Returns: (transfer full):
 *          A copy of the data selection.
 **/
GwySelection*
gwy_selection_copy(GwySelection *selection)
{
    /* Returning NULL will probably would crash something immediately, so we usually return some empty object.
     * However, Selection is an abtract type. So shrug and return NULL. */
    g_return_val_if_fail(GWY_IS_SELECTION(selection), NULL);
    return GWY_SELECTION(gwy_serializable_copy(GWY_SERIALIZABLE(selection)));
}

/**
 * gwy_selection_assign:
 * @destination: Target data selection.
 * @source: Source data selection.
 *
 * Makes one data selection equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 **/
void
gwy_selection_assign(GwySelection *destination, GwySelection *source)
{
    g_return_if_fail(GWY_IS_SELECTION(destination));
    g_return_if_fail(GWY_IS_SELECTION(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}

/**
 * SECTION: selection
 * @title: GwySelection
 * @short_description: Data selection base class
 * @see_also: #GwyVectorLayer -- uses #GwySelection for selections,
 *            #GwyGraphArea -- uses #GwySelection for selections
 *
 * #GwySelection is an abstract class representing data selections.  Particular selection types are defined by vector
 * layer modules.
 *
 * Selections behave as flat arrays of coordinates.  They are however logically split into selection objects (points,
 * lines, rectangles), characteristic for each selection type. For example, to describe a horizontal line one needs
 * only one coordinate, for a point two coordinates are needed, rectangle or arbitrary line need four.
 * gwy_selection_get_object_size() can be used to generically determine the number of coordinates used to describe
 * a one selection object.
 *
 * The number of selection objects in a selection can vary, gwy_selection_set_max_objects() sets the maximum possible
 * number.  Functions for getting and setting individual selection objects (gwy_selection_get_object(),
 * gwy_selection_set_object()) or complete selection (gwy_selection_get_data(), gwy_selection_set_data()) are
 * available. Method gwy_selection_get_n_objects() reports the number of selected objects.
 **/

/**
 * GwySelectionClass:
 * @object_size: The number of coordinates that form one selection object.
 * @crop: Virtual method cropping selection to given rectangle in the plane.
 * @move: Virtual method moving selection in the plane.
 * @changed: The "changed" signal virtual method.
 * @finished: The "finished" signal virtual method.
 *
 * The virtual methods and data memebers of #GwySelection<!-- -->s.
 *
 * Typically, the only field subclasses set in their class init method is @object_size.  The methods are implemented
 * generically in #GwySelection and need not be overriden.
 **/

/**
 * GwySelectionFilterFunc:
 * @selection: A selection.
 * @i: Index of object to consider.
 * @data: User data passed to gwy_selection_filter().
 *
 * Type of selection filtering function.
 *
 * Returns: %TRUE for objects that should be kept, %FALSE for objects that should be removed.
 **/

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